agora inbox for [email protected]
help / color / mirror / Atom feedbackend/utils/adt/float.c uses non-existent NAN value
822+ messages / 6 participants
[nested] [flat]
* backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-16 05:27 Taral <[email protected]>
0 siblings, 2 replies; 822+ messages in thread
From: Taral @ 1999-01-16 05:27 UTC (permalink / raw)
To: [email protected]
Seems at some point someone decided not to be machine-independent in
backend/utils/adt/float.c
Redhat 5.2 system with gcc 2.8.1 doesn't define NAN unless _GNU_SOURCE is
defined first. Although include/utils/dt.h *does* check for NAN (and HUGE_VAL)
before using them, the float.c doesn't.
What happened?
Taral
^ permalink raw reply [nested|flat] 822+ messages in thread
* Re: [HACKERS] backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-16 05:40 Bruce Momjian <[email protected]>
parent: Taral <[email protected]>
1 sibling, 0 replies; 822+ messages in thread
From: Bruce Momjian @ 1999-01-16 05:40 UTC (permalink / raw)
To: Taral <[email protected]>; +Cc: [email protected]
> Seems at some point someone decided not to be machine-independent in
> backend/utils/adt/float.c
>
> Redhat 5.2 system with gcc 2.8.1 doesn't define NAN unless _GNU_SOURCE is
> defined first. Although include/utils/dt.h *does* check for NAN (and HUGE_VAL)
> before using them, the float.c doesn't.
>
> What happened?
I don't see NAN used in any file except numberic.c. I don't see it in
float.c, at least in 6.5.
--
Bruce Momjian | http://www.op.net/~candle
[email protected] | (610) 853-3000
+ If your life is a hard drive, | 830 Blythe Avenue
+ Christ can be your backup. | Drexel Hill, Pennsylvania 19026
^ permalink raw reply [nested|flat] 822+ messages in thread
* Re: [HACKERS] backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-18 08:36 Jan Wieck <[email protected]>
parent: Taral <[email protected]>
1 sibling, 2 replies; 822+ messages in thread
From: Jan Wieck @ 1999-01-18 08:36 UTC (permalink / raw)
To: Taral <[email protected]>; +Cc: [email protected]
>
> Seems at some point someone decided not to be machine-independent in
> backend/utils/adt/float.c
>
> Redhat 5.2 system with gcc 2.8.1 doesn't define NAN unless _GNU_SOURCE is
> defined first. Although include/utils/dt.h *does* check for NAN (and HUGE_VAL)
> before using them, the float.c doesn't.
>
> What happened?
Don't know exactly - but
the new NUMERIC data type started recently a discussion about
NAN, because I tried to be able to convert NAN from/to
float's, and that started some trouble. I'm not sure how this
went into adt/float.c. At least I think we should fix all the
NAN handling for v6.5.
Jan
--
#======================================================================#
# It's easier to get forgiveness for being wrong than for being right. #
# Let's break this rule - forgive me. #
#======================================== [email protected] (Jan Wieck) #
^ permalink raw reply [nested|flat] 822+ messages in thread
* Re: [HACKERS] backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-18 12:27 Bruce Momjian <[email protected]>
parent: Jan Wieck <[email protected]>
1 sibling, 0 replies; 822+ messages in thread
From: Bruce Momjian @ 1999-01-18 12:27 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]
> >
> > Seems at some point someone decided not to be machine-independent in
> > backend/utils/adt/float.c
> >
> > Redhat 5.2 system with gcc 2.8.1 doesn't define NAN unless _GNU_SOURCE is
> > defined first. Although include/utils/dt.h *does* check for NAN (and HUGE_VAL)
> > before using them, the float.c doesn't.
> >
> > What happened?
>
> Don't know exactly - but
>
> the new NUMERIC data type started recently a discussion about
> NAN, because I tried to be able to convert NAN from/to
> float's, and that started some trouble. I'm not sure how this
> went into adt/float.c. At least I think we should fix all the
> NAN handling for v6.5.
>
I fixed that. Added the NAN define from numeric.c.
--
Bruce Momjian | http://www.op.net/~candle
[email protected] | (610) 853-3000
+ If your life is a hard drive, | 830 Blythe Avenue
+ Christ can be your backup. | Drexel Hill, Pennsylvania 19026
^ permalink raw reply [nested|flat] 822+ messages in thread
* Re: [HACKERS] backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-18 14:33 Thomas G. Lockhart <[email protected]>
parent: Jan Wieck <[email protected]>
1 sibling, 2 replies; 822+ messages in thread
From: Thomas G. Lockhart @ 1999-01-18 14:33 UTC (permalink / raw)
To: Jan Wieck <[email protected]>; +Cc: Taral <[email protected]>; [email protected]
> > Seems at some point someone decided not to be machine-independent in
> > backend/utils/adt/float.c
That was me.
> > Redhat 5.2 system with gcc 2.8.1 doesn't define NAN unless
> > _GNU_SOURCE is defined first. Although include/utils/dt.h *does*
> > check for NAN (and HUGE_VAL) before using them, the float.c doesn't.
> > What happened?
> the new NUMERIC data type started recently a discussion about
> NAN, because I tried to be able to convert NAN from/to
> float's, and that started some trouble. I'm not sure how this
> went into adt/float.c. At least I think we should fix all the
> NAN handling for v6.5.
Jan started with NAN handling for numeric, and I'd already done the same
thing for the date/time types quite a while ago. We also had a
discussion recently that we should be handling 'NaN' and 'Infinity' on
float8 input values so that dump/reload is more robust and symmetric, so
I added that in ('-Infinity' still needs to be done).
I added in the new code, knowing that there was some fixup and
consolidation which needs to be done. Unless someone has worked out
another scheme, I'd suggest using the date/time code in dt.h as an
example to work from. We might want to use PG_NAN and PG_INFINITY, which
we can then define in one central place.
If someone want to work this out, I'd be happy to use the result. Or I
can do something directly. I'd like a solution with the equivalent of
NAN and HUGE_VAL (and -HUGE_VAL). Jan? Taral?
- Tom
^ permalink raw reply [nested|flat] 822+ messages in thread
* Re: [HACKERS] backend/utils/adt/float.c uses non-existent NAN value
@ 1999-01-18 19:26 Taral <[email protected]>
parent: Thomas G. Lockhart <[email protected]>
1 sibling, 0 replies; 822+ messages in thread
From: Taral @ 1999-01-18 19:26 UTC (permalink / raw)
To: Thomas G. Lockhart <[email protected]>; +Cc: [email protected]
On Mon, 18 Jan 1999, you wrote:
>If someone want to work this out, I'd be happy to use the result. Or I
>can do something directly. I'd like a solution with the equivalent of
>NAN and HUGE_VAL (and -HUGE_VAL). Jan? Taral?
Doesn't dt.h have some replacements for NAN and HUGE_VAL? Something like that?
Couldn't we use those?
Aha... DT_INVALID is the generic NAN, and DT_NOBEGIN/DT_NOEND are the generic
HUGE_VAL and -HUGE_VAL...
Taral
^ permalink raw reply [nested|flat] 822+ messages in thread
* (no subject)
@ 1999-01-19 02:41 Puchong <[email protected]>
parent: Thomas G. Lockhart <[email protected]>
1 sibling, 0 replies; 822+ messages in thread
From: Puchong @ 1999-01-19 02:41 UTC (permalink / raw)
To: [email protected]
unsubscribe
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
* [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server
@ 2025-07-18 14:52 Jehan-Guillaume de Rorthais <[email protected]>
0 siblings, 0 replies; 822+ messages in thread
From: Jehan-Guillaume de Rorthais @ 2025-07-18 14:52 UTC (permalink / raw)
When a foreign table points to a partitioned table or an inheritance
parent on the foreign server, a non-direct DML can affect multiple
rows when only one row is intended to be affected. This happens
because postgres_fdw uses only ctid to identify a row to work on.
Though ctid uniquely identifies a row in a single table, in a
partitioned table or in an inheritance hierarchy, there can be be
multiple rows, in different partitions, with the same ctid. So a DML
statement sent to the foreign server by postgres_fdw ends up affecting
more than one rows, only one of which is intended to be affected.
In such a case it's good to throw an error instead of corrupting
remote database with unwanted UPDATE/DELETEs. Subsequent commits will
try to fix this situation.
Author: Ashutosh Bapat <[email protected]>
Author: Kyotaro Horiguchi <[email protected]>
Rebased by Jehan-Guillaume de Rorthais <[email protected]>
---
.../postgres_fdw/expected/postgres_fdw.out | 26 ++++++++------
contrib/postgres_fdw/postgres_fdw.c | 36 +++++++++++++++----
2 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 62019eaa881..b0ef54a2889 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -8984,10 +8984,11 @@ UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa
(5 rows)
UPDATE fa SET aa = (CASE WHEN random() <= 1 THEN 'zzzz' ELSE NULL END) WHERE aa = 'aaa';
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
-----------+-------+------
- fa | (0,2) | zzzz
+ tableoid | ctid | aa
+----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
(2 rows)
@@ -9008,11 +9009,13 @@ DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
(6 rows)
DELETE FROM fa WHERE aa = (CASE WHEN random() <= 1 THEN 'aaa' ELSE 'bbb' END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fa;
- tableoid | ctid | aa
+ tableoid | ctid | aa
----------+-------+-----
+ fa | (0,1) | aaa
fa | (0,1) | bbb
-(1 row)
+(2 rows)
-- cleanup
DROP FOREIGN TABLE fa;
@@ -9048,10 +9051,11 @@ UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
(5 rows)
UPDATE fplt SET b = (CASE WHEN random() <= 1 THEN 10 ELSE 20 END) WHERE a = 1;
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
- tableoid | ctid | a | b
-----------+-------+---+----
- fplt | (0,2) | 1 | 10
+ tableoid | ctid | a | b
+----------+-------+---+---
+ fplt | (0,1) | 1 | 1
fplt | (0,1) | 2 | 2
(2 rows)
@@ -9071,11 +9075,13 @@ DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
(6 rows)
DELETE FROM fplt WHERE a = (CASE WHEN random() <= 1 THEN 1 ELSE 10 END);
+ERROR: foreign server affected 2 rows when only one was expected
SELECT tableoid::regclass, ctid, * FROM fplt;
tableoid | ctid | a | b
----------+-------+---+---
- fplt | (0,1) | 2 | 2
-(1 row)
+ fplt | (0,1) | 1 | 1
+ fplt | (0,1) | 2 | 2
+(2 rows)
DROP TABLE plt;
DROP FOREIGN TABLE fplt;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index e0a34b27c7c..09c87d0e5d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -4132,7 +4132,8 @@ execute_foreign_modify(EState *estate,
ItemPointer ctid = NULL;
const char **p_values;
PGresult *res;
- int n_rows;
+ int n_rows_returned;
+ int n_rows_affected;
StringInfoData sql;
/* The operation should be INSERT, UPDATE, or DELETE */
@@ -4213,27 +4214,50 @@ execute_foreign_modify(EState *estate,
pgfdw_report_error(ERROR, res, fmstate->conn, true, fmstate->query);
/* Check number of rows affected, and fetch RETURNING tuple if any */
+ n_rows_affected = atoi(PQcmdTuples(res));
if (fmstate->has_returning)
{
Assert(*numSlots == 1);
- n_rows = PQntuples(res);
- if (n_rows > 0)
+ n_rows_returned = PQntuples(res);
+ if (n_rows_returned > 0)
store_returning_result(fmstate, slots[0], res);
+
+ // FIXME: shouldn't we check the max number of rows returned is one?
}
else
- n_rows = atoi(PQcmdTuples(res));
+ n_rows_returned = 0;
/* And clean up */
PQclear(res);
MemoryContextReset(fmstate->temp_cxt);
- *numSlots = n_rows;
+ /*
+ * UPDATE & DELETE command can only affect one row, make sure this contract
+ * is respected.
+ * CMD_INSERT can insert multiple row when called from ForeignBatchInsert.
+ */
+ if (operation != CMD_INSERT)
+ {
+ /* No rows should be returned if no rows were affected */
+ if (n_rows_affected == 0 && n_rows_returned != 0)
+ elog(ERROR, "foreign server returned %d rows when no row was affected",
+ n_rows_returned);
+
+ /* ERROR if more than one row was updated on the remote end */
+ if (n_rows_affected > 1)
+ ereport(ERROR,
+ (errcode (ERRCODE_FDW_ERROR), /* XXX */
+ errmsg ("foreign server affected %d rows when only one was expected",
+ n_rows_affected)));
+ }
+
+ *numSlots = n_rows_returned;
/*
* Return NULL if nothing was inserted/updated/deleted on the remote end
*/
- return (n_rows > 0) ? slots : NULL;
+ return (n_rows_affected > 0) ? slots : NULL;
}
/*
--
2.50.0
--MP_/4ZRYdF7Ah.pt5w65PuZdaPz--
^ permalink raw reply [nested|flat] 822+ messages in thread
end of thread, other threads:[~2025-07-18 14:52 UTC | newest]
Thread overview: 822+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
1999-01-16 05:27 backend/utils/adt/float.c uses non-existent NAN value Taral <[email protected]>
1999-01-16 05:40 ` Bruce Momjian <[email protected]>
1999-01-18 08:36 ` Jan Wieck <[email protected]>
1999-01-18 12:27 ` Bruce Momjian <[email protected]>
1999-01-18 14:33 ` Thomas G. Lockhart <[email protected]>
1999-01-18 19:26 ` Taral <[email protected]>
1999-01-19 02:41 ` (no subject) Puchong <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] Error out if one iteration of non-direct DML affects more than one row on the foreign server Jehan-Guillaume de Rorthais <[email protected]>
2025-07-18 14:52 [PATCH v3 2/2] 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