public inbox for [email protected]
help / color / mirror / Atom feed[PATCH v5 1/2] In-place table persistence change
14+ messages / 6 participants
[nested] [flat]
* [PATCH v5 1/2] In-place table persistence change
@ 2020-11-11 12:51 Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 14+ messages in thread
From: Kyotaro Horiguchi @ 2020-11-11 12:51 UTC (permalink / raw)
Even though ALTER TABLE SET LOGGED/UNLOGGED does not require data
rewriting, currently it runs heap rewrite which causes large amount of
file I/O. This patch makes the command run without heap rewrite.
Addition to that, SET LOGGED while wal_level > minimal emits WAL using
XLOG_FPI instead of massive number of HEAP_INSERT's, which should be
smaller.
Also this allows for the cleanup of files left behind in the crash of
the transaction that created it.
---
src/backend/access/rmgrdesc/smgrdesc.c | 23 ++
src/backend/access/transam/README | 8 +
src/backend/access/transam/xlog.c | 17 +
src/backend/catalog/storage.c | 436 +++++++++++++++++++++++--
src/backend/commands/tablecmds.c | 246 +++++++++++---
src/backend/storage/buffer/bufmgr.c | 88 +++++
src/backend/storage/file/reinit.c | 316 ++++++++++++------
src/backend/storage/smgr/md.c | 13 +-
src/backend/storage/smgr/smgr.c | 6 +
src/common/relpath.c | 4 +-
src/include/catalog/storage.h | 2 +
src/include/catalog/storage_xlog.h | 22 +-
src/include/common/relpath.h | 6 +-
src/include/storage/bufmgr.h | 2 +
src/include/storage/md.h | 2 +
src/include/storage/reinit.h | 3 +-
src/include/storage/smgr.h | 1 +
17 files changed, 1028 insertions(+), 167 deletions(-)
diff --git a/src/backend/access/rmgrdesc/smgrdesc.c b/src/backend/access/rmgrdesc/smgrdesc.c
index 7755553d57..2c109b8ca4 100644
--- a/src/backend/access/rmgrdesc/smgrdesc.c
+++ b/src/backend/access/rmgrdesc/smgrdesc.c
@@ -40,6 +40,23 @@ smgr_desc(StringInfo buf, XLogReaderState *record)
xlrec->blkno, xlrec->flags);
pfree(path);
}
+ else if (info == XLOG_SMGR_UNLINK)
+ {
+ xl_smgr_unlink *xlrec = (xl_smgr_unlink *) rec;
+ char *path = relpathperm(xlrec->rnode, xlrec->forkNum);
+
+ appendStringInfoString(buf, path);
+ pfree(path);
+ }
+ else if (info == XLOG_SMGR_BUFPERSISTENCE)
+ {
+ xl_smgr_bufpersistence *xlrec = (xl_smgr_bufpersistence *) rec;
+ char *path = relpathperm(xlrec->rnode, MAIN_FORKNUM);
+
+ appendStringInfoString(buf, path);
+ appendStringInfo(buf, " persistence %d", xlrec->persistence);
+ pfree(path);
+ }
}
const char *
@@ -55,6 +72,12 @@ smgr_identify(uint8 info)
case XLOG_SMGR_TRUNCATE:
id = "TRUNCATE";
break;
+ case XLOG_SMGR_UNLINK:
+ id = "UNLINK";
+ break;
+ case XLOG_SMGR_BUFPERSISTENCE:
+ id = "BUFPERSISTENCE";
+ break;
}
return id;
diff --git a/src/backend/access/transam/README b/src/backend/access/transam/README
index 1edc8180c1..547107a771 100644
--- a/src/backend/access/transam/README
+++ b/src/backend/access/transam/README
@@ -724,6 +724,14 @@ we must panic and abort recovery. The DBA will have to manually clean up
then restart recovery. This is part of the reason for not writing a WAL
entry until we've successfully done the original action.
+The CLEANUP fork file
+--------------------------------
+
+An CLEANUP fork is created when a new relation file is created to mark
+the relfilenode needs to be cleaned up at recovery time. In contrast
+to 4 above, failure to remove an CLEANUP fork file will lead to data
+loss, in which case the server will shut down.
+
Skipping WAL for New RelFileNode
--------------------------------
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index b18257c198..6dcbcbe387 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -40,6 +40,7 @@
#include "catalog/catversion.h"
#include "catalog/pg_control.h"
#include "catalog/pg_database.h"
+#include "catalog/storage.h"
#include "commands/progress.h"
#include "commands/tablespace.h"
#include "common/controldata_utils.h"
@@ -4442,6 +4443,14 @@ ReadRecord(XLogReaderState *xlogreader, int emode,
{
ereport(DEBUG1,
(errmsg_internal("reached end of WAL in pg_wal, entering archive recovery")));
+
+ /* cleanup garbage files left during crash recovery */
+ ResetUnloggedRelations(UNLOGGED_RELATION_DROP_BUFFER |
+ UNLOGGED_RELATION_CLEANUP);
+
+ /* run rollback cleanup if any */
+ smgrDoPendingDeletes(false);
+
InArchiveRecovery = true;
if (StandbyModeRequested)
StandbyMode = true;
@@ -7455,6 +7464,14 @@ StartupXLOG(void)
}
}
+ /* cleanup garbage files left during crash recovery */
+ if (!InArchiveRecovery)
+ ResetUnloggedRelations(UNLOGGED_RELATION_DROP_BUFFER |
+ UNLOGGED_RELATION_CLEANUP);
+
+ /* run rollback cleanup if any */
+ smgrDoPendingDeletes(false);
+
/* Allow resource managers to do any required cleanup. */
for (rmid = 0; rmid <= RM_MAX_ID; rmid++)
{
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index cba7a9ada0..c54d70747f 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -19,6 +19,7 @@
#include "postgres.h"
+#include "access/amapi.h"
#include "access/parallel.h"
#include "access/visibilitymap.h"
#include "access/xact.h"
@@ -27,6 +28,7 @@
#include "access/xlogutils.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
+#include "common/hashfn.h"
#include "miscadmin.h"
#include "storage/freespace.h"
#include "storage/smgr.h"
@@ -57,9 +59,16 @@ int wal_skip_threshold = 2048; /* in kilobytes */
* but I'm being paranoid.
*/
+#define PDOP_DELETE (0)
+#define PDOP_UNLINK_FORK (1 << 0)
+#define PDOP_SET_PERSISTENCE (1 << 1)
+
typedef struct PendingRelDelete
{
RelFileNode relnode; /* relation that may need to be deleted */
+ int op; /* operation mask */
+ bool bufpersistence; /* buffer persistence to set */
+ int unlink_forknum; /* forknum to unlink */
BackendId backend; /* InvalidBackendId if not a temp rel */
bool atCommit; /* T=delete at commit; F=delete at abort */
int nestLevel; /* xact nesting level of request */
@@ -75,6 +84,24 @@ typedef struct PendingRelSync
static PendingRelDelete *pendingDeletes = NULL; /* head of linked list */
HTAB *pendingSyncHash = NULL;
+typedef struct SRelHashEntry
+{
+ SMgrRelation srel;
+ char status; /* for simplehash use */
+} SRelHashEntry;
+
+/* define hashtable for workarea for pending deletes */
+#define SH_PREFIX srelhash
+#define SH_ELEMENT_TYPE SRelHashEntry
+#define SH_KEY_TYPE SMgrRelation
+#define SH_KEY srel
+#define SH_HASH_KEY(tb, key) \
+ hash_bytes((unsigned char *)&key, sizeof(SMgrRelation))
+#define SH_EQUAL(tb, a, b) (memcmp(&a, &b, sizeof(SMgrRelation)) == 0)
+#define SH_SCOPE static inline
+#define SH_DEFINE
+#define SH_DECLARE
+#include "lib/simplehash.h"
/*
* AddPendingSync
@@ -143,7 +170,17 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
return NULL; /* placate compiler */
}
+ /*
+ * We are going to create a new storage file. If server crashes before the
+ * current transaction ends the file needs to be cleaned up but there's no
+ * clue to the orphan files. The cleanup fork works as the sentinel to
+ * identify that situation.
+ */
srel = smgropen(rnode, backend);
+ smgrcreate(srel, CLEANUP2_FORKNUM, false);
+ log_smgrcreate(&rnode, CLEANUP2_FORKNUM);
+ smgrimmedsync(srel, CLEANUP2_FORKNUM);
+
smgrcreate(srel, MAIN_FORKNUM, false);
if (needs_wal)
@@ -153,12 +190,25 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
pending = (PendingRelDelete *)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->relnode = rnode;
+ pending->op = PDOP_DELETE;
pending->backend = backend;
pending->atCommit = false; /* delete if abort */
pending->nestLevel = GetCurrentTransactionNestLevel();
pending->next = pendingDeletes;
pendingDeletes = pending;
+ /* drop cleanup fork at commit */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_UNLINK_FORK;
+ pending->unlink_forknum = CLEANUP2_FORKNUM;
+ pending->backend = backend;
+ pending->atCommit = true;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+
if (relpersistence == RELPERSISTENCE_PERMANENT && !XLogIsNeeded())
{
Assert(backend == InvalidBackendId);
@@ -168,6 +218,218 @@ RelationCreateStorage(RelFileNode rnode, char relpersistence)
return srel;
}
+/*
+ * RelationCreateInitFork
+ * Create physical storage for the init fork of a relation.
+ *
+ * Create the init fork for the relation.
+ *
+ * This function is transactional. The creation is WAL-logged, and if the
+ * transaction aborts later on, the init fork will be removed.
+ */
+void
+RelationCreateInitFork(Relation rel)
+{
+ RelFileNode rnode = rel->rd_node;
+ PendingRelDelete *pending;
+ SMgrRelation srel;
+ PendingRelDelete *prev;
+ PendingRelDelete *next;
+ bool create = true;
+
+ /* switch buffer persistence */
+ SetRelationBuffersPersistence(rel->rd_smgr, false, false);
+
+ /*
+ * If we have entries for init-fork operation of this relation, that means
+ * that we have already registered pending delete entries to drop
+ * preexisting init fork since before the current transaction started. This
+ * function reverts that change just by removing the entries.
+ */
+ prev = NULL;
+ for (pending = pendingDeletes; pending != NULL; pending = next)
+ {
+ next = pending->next;
+ if (RelFileNodeEquals(rnode, pending->relnode) &&
+ pending->op != PDOP_DELETE &&
+ ((pending->op & PDOP_UNLINK_FORK) != 0 &&
+ pending->unlink_forknum == CLEANUP_FORKNUM))
+ {
+ if (prev)
+ prev->next = next;
+ else
+ pendingDeletes = next;
+ pfree(pending);
+
+ create = false;
+ }
+ else
+ {
+ /* unrelated entry, don't touch it */
+ prev = pending;
+ }
+ }
+
+ if (!create)
+ return;
+
+ /*
+ * We are going to create the init fork. If server crashes before the
+ * current transaction ends the init fork left alone corrupts data while
+ * recovery. The cleanup fork works as the sentinel to identify that
+ * situation.
+ */
+ srel = smgropen(rnode, InvalidBackendId);
+ smgrcreate(srel, CLEANUP_FORKNUM, false);
+ log_smgrcreate(&rnode, CLEANUP_FORKNUM);
+ smgrimmedsync(srel, CLEANUP_FORKNUM);
+
+ /* We don't have existing init fork, create it. */
+ smgrcreate(srel, INIT_FORKNUM, false);
+
+ /*
+ * index-init fork needs further initialization. ambuildempty shoud do
+ * WAL-log and file sync by itself but otherwise we do that by myself.
+ */
+ if (rel->rd_rel->relkind == RELKIND_INDEX)
+ rel->rd_indam->ambuildempty(rel);
+ else
+ {
+ log_smgrcreate(&rnode, INIT_FORKNUM);
+ smgrimmedsync(srel, INIT_FORKNUM);
+ }
+
+ /* drop this init fork file at abort and revert persistence */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_UNLINK_FORK | PDOP_SET_PERSISTENCE;
+ pending->unlink_forknum = INIT_FORKNUM;
+ pending->bufpersistence = true;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = false;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+
+ /* drop cleanup fork at abort */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_UNLINK_FORK;
+ pending->unlink_forknum = CLEANUP_FORKNUM;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = false;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+
+ /* drop cleanup fork at commit */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_UNLINK_FORK;
+ pending->unlink_forknum = CLEANUP_FORKNUM;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = true;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+}
+
+/*
+ * RelationDropInitFork
+ * Delete physical storage for the init fork of a relation.
+ *
+ * Register pending-delete of the init fork. The real deletion is performed by
+ * smgrDoPendingDeletes at commit.
+ *
+ * This function is transactional. If the transaction aborts later on, the
+ * deletion doesn't happen.
+ */
+void
+RelationDropInitFork(Relation rel)
+{
+ RelFileNode rnode = rel->rd_node;
+ PendingRelDelete *pending;
+ PendingRelDelete *prev;
+ PendingRelDelete *next;
+ bool inxact_created = false;
+
+ /* switch buffer persistence */
+ SetRelationBuffersPersistence(rel->rd_smgr, true, false);
+
+ /*
+ * If we have entries for init-fork operation of this relation, that means
+ * that we have created the init fork in the current transaction. We
+ * immediately remove the init and cleanup forks immediately in that case.
+ * Otherwise just reister pending-delete for the existing init fork.
+ */
+ prev = NULL;
+ for (pending = pendingDeletes; pending != NULL; pending = next)
+ {
+ next = pending->next;
+ if (RelFileNodeEquals(rnode, pending->relnode) &&
+ pending->op != PDOP_DELETE &&
+ ((pending->op & PDOP_UNLINK_FORK) != 0 &&
+ pending->unlink_forknum == CLEANUP_FORKNUM))
+ {
+ /* unlink list entry */
+ if (prev)
+ prev->next = next;
+ else
+ pendingDeletes = next;
+ pfree(pending);
+
+ inxact_created = true;
+ }
+ else
+ {
+ /* unrelated entry, don't touch it */
+ prev = pending;
+ }
+ }
+
+ if (inxact_created)
+ {
+ SMgrRelation srel = smgropen(rnode, InvalidBackendId);
+
+ /*
+ * INIT/CLEANUP forks never be loaded to shared buffer so no point in
+ * dropping buffers for these files.
+ */
+ log_smgrunlink(&rnode, INIT_FORKNUM);
+ smgrunlink(srel, INIT_FORKNUM, false);
+ log_smgrunlink(&rnode, CLEANUP_FORKNUM);
+ smgrunlink(srel, CLEANUP_FORKNUM, false);
+ return;
+ }
+
+ /* register drop of this init fork file at commit */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_UNLINK_FORK;
+ pending->unlink_forknum = INIT_FORKNUM;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = true;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+
+ /* revert buffer-persistence changes at abort */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = rnode;
+ pending->op = PDOP_SET_PERSISTENCE;
+ pending->bufpersistence = false;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = false;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+}
+
/*
* Perform XLogInsert of an XLOG_SMGR_CREATE record to WAL.
*/
@@ -187,6 +449,44 @@ log_smgrcreate(const RelFileNode *rnode, ForkNumber forkNum)
XLogInsert(RM_SMGR_ID, XLOG_SMGR_CREATE | XLR_SPECIAL_REL_UPDATE);
}
+/*
+ * Perform XLogInsert of an XLOG_SMGR_UNLINK record to WAL.
+ */
+void
+log_smgrunlink(const RelFileNode *rnode, ForkNumber forkNum)
+{
+ xl_smgr_unlink xlrec;
+
+ /*
+ * Make an XLOG entry reporting the file unlink.
+ */
+ xlrec.rnode = *rnode;
+ xlrec.forkNum = forkNum;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xlrec));
+ XLogInsert(RM_SMGR_ID, XLOG_SMGR_UNLINK | XLR_SPECIAL_REL_UPDATE);
+}
+
+/*
+ * Perform XLogInsert of an XLOG_SMGR_BUFPERSISTENCE record to WAL.
+ */
+void
+log_smgrbufpersistence(const RelFileNode *rnode, bool persistence)
+{
+ xl_smgr_bufpersistence xlrec;
+
+ /*
+ * Make an XLOG entry reporting the change of buffer persistence.
+ */
+ xlrec.rnode = *rnode;
+ xlrec.persistence = persistence;
+
+ XLogBeginInsert();
+ XLogRegisterData((char *) &xlrec, sizeof(xlrec));
+ XLogInsert(RM_SMGR_ID, XLOG_SMGR_BUFPERSISTENCE | XLR_SPECIAL_REL_UPDATE);
+}
+
/*
* RelationDropStorage
* Schedule unlinking of physical storage at transaction commit.
@@ -200,6 +500,7 @@ RelationDropStorage(Relation rel)
pending = (PendingRelDelete *)
MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
pending->relnode = rel->rd_node;
+ pending->op = PDOP_DELETE;
pending->backend = rel->rd_backend;
pending->atCommit = true; /* delete if commit */
pending->nestLevel = GetCurrentTransactionNestLevel();
@@ -602,59 +903,97 @@ smgrDoPendingDeletes(bool isCommit)
int nrels = 0,
maxrels = 0;
SMgrRelation *srels = NULL;
+ srelhash_hash *close_srels = NULL;
+ bool found;
prev = NULL;
for (pending = pendingDeletes; pending != NULL; pending = next)
{
+ SMgrRelation srel;
+
next = pending->next;
if (pending->nestLevel < nestLevel)
{
/* outer-level entries should not be processed yet */
prev = pending;
+ continue;
}
+
+ /* unlink list entry first, so we don't retry on failure */
+ if (prev)
+ prev->next = next;
else
+ pendingDeletes = next;
+
+ if (pending->atCommit != isCommit)
{
- /* unlink list entry first, so we don't retry on failure */
- if (prev)
- prev->next = next;
- else
- pendingDeletes = next;
- /* do deletion if called for */
- if (pending->atCommit == isCommit)
- {
- SMgrRelation srel;
-
- srel = smgropen(pending->relnode, pending->backend);
-
- /* allocate the initial array, or extend it, if needed */
- if (maxrels == 0)
- {
- maxrels = 8;
- srels = palloc(sizeof(SMgrRelation) * maxrels);
- }
- else if (maxrels <= nrels)
- {
- maxrels *= 2;
- srels = repalloc(srels, sizeof(SMgrRelation) * maxrels);
- }
-
- srels[nrels++] = srel;
- }
/* must explicitly free the list entry */
pfree(pending);
/* prev does not change */
+ continue;
+ }
+
+ if (close_srels == NULL)
+ close_srels = srelhash_create(CurrentMemoryContext, 32, NULL);
+
+ srel = smgropen(pending->relnode, pending->backend);
+
+ /* Uniquify the smgr relations */
+ srelhash_insert(close_srels, srel, &found);
+
+ if (pending->op != PDOP_DELETE)
+ {
+ if (pending->op & PDOP_UNLINK_FORK)
+ {
+ /* other forks needs to drop buffers */
+ Assert(pending->unlink_forknum == INIT_FORKNUM ||
+ pending->unlink_forknum == CLEANUP_FORKNUM ||
+ pending->unlink_forknum == CLEANUP2_FORKNUM);
+
+ log_smgrunlink(&pending->relnode, pending->unlink_forknum);
+ smgrunlink(srel, pending->unlink_forknum, false);
+
+ }
+
+ if (pending->op & PDOP_SET_PERSISTENCE)
+ SetRelationBuffersPersistence(srel, pending->bufpersistence,
+ InRecovery);
+ }
+ else
+ {
+ /* allocate the initial array, or extend it, if needed */
+ if (maxrels == 0)
+ {
+ maxrels = 8;
+ srels = palloc(sizeof(SMgrRelation) * maxrels);
+ }
+ else if (maxrels <= nrels)
+ {
+ maxrels *= 2;
+ srels = repalloc(srels, sizeof(SMgrRelation) * maxrels);
+ }
+
+ srels[nrels++] = srel;
}
}
if (nrels > 0)
{
smgrdounlinkall(srels, nrels, false);
-
- for (int i = 0; i < nrels; i++)
- smgrclose(srels[i]);
-
pfree(srels);
}
+
+ if (close_srels)
+ {
+ srelhash_iterator i;
+ SRelHashEntry *ent;
+
+ /* close smgr relatoins */
+ srelhash_start_iterate(close_srels, &i);
+ while ((ent = srelhash_iterate(close_srels, &i)) != NULL)
+ smgrclose(ent->srel);
+ srelhash_destroy(close_srels);
+ }
}
/*
@@ -824,7 +1163,8 @@ smgrGetPendingDeletes(bool forCommit, RelFileNode **ptr)
for (pending = pendingDeletes; pending != NULL; pending = pending->next)
{
if (pending->nestLevel >= nestLevel && pending->atCommit == forCommit
- && pending->backend == InvalidBackendId)
+ && pending->backend == InvalidBackendId
+ && pending->op == PDOP_DELETE)
nrels++;
}
if (nrels == 0)
@@ -837,7 +1177,8 @@ smgrGetPendingDeletes(bool forCommit, RelFileNode **ptr)
for (pending = pendingDeletes; pending != NULL; pending = pending->next)
{
if (pending->nestLevel >= nestLevel && pending->atCommit == forCommit
- && pending->backend == InvalidBackendId)
+ && pending->backend == InvalidBackendId &&
+ pending->op == PDOP_DELETE)
{
*rptr = pending->relnode;
rptr++;
@@ -917,6 +1258,15 @@ smgr_redo(XLogReaderState *record)
reln = smgropen(xlrec->rnode, InvalidBackendId);
smgrcreate(reln, xlrec->forkNum, true);
}
+ else if (info == XLOG_SMGR_UNLINK)
+ {
+ xl_smgr_unlink *xlrec = (xl_smgr_unlink *) XLogRecGetData(record);
+ SMgrRelation reln;
+
+ reln = smgropen(xlrec->rnode, InvalidBackendId);
+ smgrunlink(reln, xlrec->forkNum, true);
+ smgrclose(reln);
+ }
else if (info == XLOG_SMGR_TRUNCATE)
{
xl_smgr_truncate *xlrec = (xl_smgr_truncate *) XLogRecGetData(record);
@@ -1005,6 +1355,28 @@ smgr_redo(XLogReaderState *record)
FreeFakeRelcacheEntry(rel);
}
+ else if (info == XLOG_SMGR_BUFPERSISTENCE)
+ {
+ xl_smgr_bufpersistence *xlrec =
+ (xl_smgr_bufpersistence *) XLogRecGetData(record);
+ SMgrRelation reln;
+ PendingRelDelete *pending;
+
+ reln = smgropen(xlrec->rnode, InvalidBackendId);
+ SetRelationBuffersPersistence(reln, xlrec->persistence, true);
+
+ /* revert buffer-persistence changes at abort */
+ pending = (PendingRelDelete *)
+ MemoryContextAlloc(TopMemoryContext, sizeof(PendingRelDelete));
+ pending->relnode = xlrec->rnode;
+ pending->op = PDOP_SET_PERSISTENCE;
+ pending->bufpersistence = !xlrec->persistence;
+ pending->backend = InvalidBackendId;
+ pending->atCommit = false;
+ pending->nestLevel = GetCurrentTransactionNestLevel();
+ pending->next = pendingDeletes;
+ pendingDeletes = pending;
+ }
else
elog(PANIC, "smgr_redo: unknown op code %u", info);
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 993da56d43..37a15d31ee 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -50,6 +50,7 @@
#include "commands/defrem.h"
#include "commands/event_trigger.h"
#include "commands/policy.h"
+#include "commands/progress.h"
#include "commands/sequence.h"
#include "commands/tablecmds.h"
#include "commands/tablespace.h"
@@ -4917,6 +4918,170 @@ ATParseTransformCmd(List **wqueue, AlteredTableInfo *tab, Relation rel,
return newcmd;
}
+/*
+ * RelationChangePersistence: do in-place persistence change of a relation
+ */
+static void
+RelationChangePersistence(AlteredTableInfo *tab, char persistence,
+ LOCKMODE lockmode)
+{
+ Relation rel;
+ Relation classRel;
+ HeapTuple tuple,
+ newtuple;
+ Datum new_val[Natts_pg_class];
+ bool new_null[Natts_pg_class],
+ new_repl[Natts_pg_class];
+ int i;
+ List *relids;
+ ListCell *lc_oid;
+
+ Assert(tab->rewrite == AT_REWRITE_ALTER_PERSISTENCE);
+ Assert(lockmode == AccessExclusiveLock);
+
+ /*
+ * Under the following condition, we need to call ATRewriteTable, which
+ * cannot be false in the AT_REWRITE_ALTER_PERSISTENCE case.
+ */
+ Assert(tab->constraints == NULL && tab->partition_constraint == NULL &&
+ tab->newvals == NULL && !tab->verify_new_notnull);
+
+ rel = table_open(tab->relid, lockmode);
+
+ Assert(rel->rd_rel->relpersistence != persistence);
+
+ elog(DEBUG1, "perform im-place persistnce change");
+
+ RelationOpenSmgr(rel);
+
+ /*
+ * First we collect all relations that we need to change persistence.
+ */
+
+ /* Collect OIDs of indexes and toast relations */
+ relids = RelationGetIndexList(rel);
+ relids = lcons_oid(rel->rd_id, relids);
+
+ /* Add toast relation if any */
+ if (OidIsValid(rel->rd_rel->reltoastrelid))
+ {
+ List *toastidx;
+ Relation toastrel = table_open(rel->rd_rel->reltoastrelid, lockmode);
+
+ RelationOpenSmgr(toastrel);
+ relids = lappend_oid(relids, rel->rd_rel->reltoastrelid);
+ toastidx = RelationGetIndexList(toastrel);
+ relids = list_concat(relids, toastidx);
+ pfree(toastidx);
+ table_close(toastrel, NoLock);
+ }
+
+ table_close(rel, NoLock);
+
+ /* Make changes in storage */
+ classRel = table_open(RelationRelationId, RowExclusiveLock);
+
+ foreach (lc_oid, relids)
+ {
+ Oid reloid = lfirst_oid(lc_oid);
+ Relation r = relation_open(reloid, lockmode);
+
+ /*
+ * Some access methods do not accept in-place persistence change. For
+ * example, GiST uses page LSNs to figure out whether a block has
+ * changed, where UNLOGGED GiST indexes use fake LSNs that are
+ * incompatible with real LSNs used for LOGGED ones.
+ *
+ * XXXX: We don't bother allowing in-place persistence change for index
+ * methods other than btree for now.
+ */
+ if (r->rd_rel->relkind == RELKIND_INDEX &&
+ r->rd_rel->relam != BTREE_AM_OID)
+ {
+ int reindex_flags;
+
+ /* reindex doesn't allow concurrent use of the index */
+ table_close(r, NoLock);
+
+ reindex_flags =
+ REINDEX_REL_SUPPRESS_INDEX_USE |
+ REINDEX_REL_CHECK_CONSTRAINTS;
+
+ /* Set the same persistence with the parent relation. */
+ if (persistence == RELPERSISTENCE_UNLOGGED)
+ reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
+ else
+ reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
+
+ reindex_index(reloid, reindex_flags, persistence, 0);
+
+ continue;
+ }
+
+ RelationOpenSmgr(r);
+
+ /* Create or drop init fork */
+ if (persistence == RELPERSISTENCE_UNLOGGED)
+ RelationCreateInitFork(r);
+ else
+ RelationDropInitFork(r);
+
+ /*
+ * When this relation gets WAL-logged, immediately sync all files but
+ * initfork to establish the initial state on storage. Buffers have
+ * already flushed out by RelationCreate(Drop)InitFork called just
+ * above. Initfork should have been synced as needed.
+ */
+ if (persistence == RELPERSISTENCE_PERMANENT)
+ {
+ for (i = 0 ; i < INIT_FORKNUM ; i++)
+ {
+ if (smgrexists(r->rd_smgr, i))
+ smgrimmedsync(r->rd_smgr, i);
+ }
+ }
+
+ /* Update catalog */
+ tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", reloid);
+
+ memset(new_val, 0, sizeof(new_val));
+ memset(new_null, false, sizeof(new_null));
+ memset(new_repl, false, sizeof(new_repl));
+
+ new_val[Anum_pg_class_relpersistence - 1] = CharGetDatum(persistence);
+ new_null[Anum_pg_class_relpersistence - 1] = false;
+ new_repl[Anum_pg_class_relpersistence - 1] = true;
+
+ newtuple = heap_modify_tuple(tuple, RelationGetDescr(classRel),
+ new_val, new_null, new_repl);
+
+ CatalogTupleUpdate(classRel, &newtuple->t_self, newtuple);
+ heap_freetuple(newtuple);
+
+ /*
+ * While wal_level >= replica, switching to LOGGED requires the
+ * relation content to be WAL-logged to recover the table.
+ */
+ if (persistence == RELPERSISTENCE_PERMANENT && XLogIsNeeded())
+ {
+ ForkNumber fork;
+
+ for (fork = 0; fork < INIT_FORKNUM ; fork++)
+ {
+ if (smgrexists(r->rd_smgr, fork))
+ log_newpage_range(r, fork,
+ 0, smgrnblocks(r->rd_smgr, fork), false);
+ }
+ }
+
+ table_close(r, NoLock);
+ }
+
+ table_close(classRel, NoLock);
+}
+
/*
* ATRewriteTables: ALTER TABLE phase 3
*/
@@ -5037,45 +5202,52 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
tab->relid,
tab->rewrite);
- /*
- * Create transient table that will receive the modified data.
- *
- * Ensure it is marked correctly as logged or unlogged. We have
- * to do this here so that buffers for the new relfilenode will
- * have the right persistence set, and at the same time ensure
- * that the original filenode's buffers will get read in with the
- * correct setting (i.e. the original one). Otherwise a rollback
- * after the rewrite would possibly result with buffers for the
- * original filenode having the wrong persistence setting.
- *
- * NB: This relies on swap_relation_files() also swapping the
- * persistence. That wouldn't work for pg_class, but that can't be
- * unlogged anyway.
- */
- OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, persistence,
- lockmode);
+ if (tab->rewrite == AT_REWRITE_ALTER_PERSISTENCE)
+ RelationChangePersistence(tab, persistence, lockmode);
+ else
+ {
+ /*
+ * Create transient table that will receive the modified data.
+ *
+ * Ensure it is marked correctly as logged or unlogged. We
+ * have to do this here so that buffers for the new relfilenode
+ * will have the right persistence set, and at the same time
+ * ensure that the original filenode's buffers will get read in
+ * with the correct setting (i.e. the original one). Otherwise
+ * a rollback after the rewrite would possibly result with
+ * buffers for the original filenode having the wrong
+ * persistence setting.
+ *
+ * NB: This relies on swap_relation_files() also swapping the
+ * persistence. That wouldn't work for pg_class, but that can't
+ * be unlogged anyway.
+ */
+ OIDNewHeap = make_new_heap(tab->relid, NewTableSpace, persistence,
+ lockmode);
- /*
- * Copy the heap data into the new table with the desired
- * modifications, and test the current data within the table
- * against new constraints generated by ALTER TABLE commands.
- */
- ATRewriteTable(tab, OIDNewHeap, lockmode);
+ /*
+ * Copy the heap data into the new table with the desired
+ * modifications, and test the current data within the table
+ * against new constraints generated by ALTER TABLE commands.
+ */
+ ATRewriteTable(tab, OIDNewHeap, lockmode);
- /*
- * Swap the physical files of the old and new heaps, then rebuild
- * indexes and discard the old heap. We can use RecentXmin for
- * the table's new relfrozenxid because we rewrote all the tuples
- * in ATRewriteTable, so no older Xid remains in the table. Also,
- * we never try to swap toast tables by content, since we have no
- * interest in letting this code work on system catalogs.
- */
- finish_heap_swap(tab->relid, OIDNewHeap,
- false, false, true,
- !OidIsValid(tab->newTableSpace),
- RecentXmin,
- ReadNextMultiXactId(),
- persistence);
+ /*
+ * Swap the physical files of the old and new heaps, then
+ * rebuild indexes and discard the old heap. We can use
+ * RecentXmin for the table's new relfrozenxid because we
+ * rewrote all the tuples in ATRewriteTable, so no older Xid
+ * remains in the table. Also, we never try to swap toast
+ * tables by content, since we have no interest in letting this
+ * code work on system catalogs.
+ */
+ finish_heap_swap(tab->relid, OIDNewHeap,
+ false, false, true,
+ !OidIsValid(tab->newTableSpace),
+ RecentXmin,
+ ReadNextMultiXactId(),
+ persistence);
+ }
}
else
{
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 561c212092..eacbdc6447 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -37,6 +37,7 @@
#include "access/xlog.h"
#include "catalog/catalog.h"
#include "catalog/storage.h"
+#include "catalog/storage_xlog.h"
#include "executor/instrument.h"
#include "lib/binaryheap.h"
#include "miscadmin.h"
@@ -3094,6 +3095,93 @@ DropRelFileNodeBuffers(SMgrRelation smgr_reln, ForkNumber *forkNum,
}
}
+/* ---------------------------------------------------------------------
+ * SetRelFileNodeBuffersPersistence
+ *
+ * This function changes the persistence of all buffer pages of a relation
+ * then writes all dirty pages of the relation out to disk when switching
+ * to PERMANENT. (or more accurately, out to kernel disk buffers),
+ * ensuring that the kernel has an up-to-date view of the relation.
+ *
+ * Generally, the caller should be holding AccessExclusiveLock on the
+ * target relation to ensure that no other backend is busy dirtying
+ * more blocks of the relation; the effects can't be expected to last
+ * after the lock is released.
+ *
+ * XXX currently it sequentially searches the buffer pool, should be
+ * changed to more clever ways of searching. This routine is not
+ * used in any performance-critical code paths, so it's not worth
+ * adding additional overhead to normal paths to make it go faster;
+ * but see also DropRelFileNodeBuffers.
+ * --------------------------------------------------------------------
+ */
+void
+SetRelationBuffersPersistence(SMgrRelation srel, bool permanent, bool isRedo)
+{
+ int i;
+ RelFileNodeBackend rnode = srel->smgr_rnode;
+
+ Assert (!RelFileNodeBackendIsTemp(rnode));
+
+ if (!isRedo)
+ log_smgrbufpersistence(&srel->smgr_rnode.node, permanent);
+
+ ResourceOwnerEnlargeBuffers(CurrentResourceOwner);
+
+ for (i = 0; i < NBuffers; i++)
+ {
+ BufferDesc *bufHdr = GetBufferDescriptor(i);
+ uint32 buf_state;
+
+ if (!RelFileNodeEquals(bufHdr->tag.rnode, rnode.node))
+ continue;
+
+ ReservePrivateRefCountEntry();
+
+ buf_state = LockBufHdr(bufHdr);
+
+ if (!RelFileNodeEquals(bufHdr->tag.rnode, rnode.node))
+ {
+ UnlockBufHdr(bufHdr, buf_state);
+ continue;
+ }
+
+ if (permanent)
+ {
+ /* Init fork is being dropped, drop buffers for it. */
+ if (bufHdr->tag.forkNum == INIT_FORKNUM)
+ {
+ InvalidateBuffer(bufHdr);
+ continue;
+ }
+
+ buf_state |= BM_PERMANENT;
+ pg_atomic_write_u32(&bufHdr->state, buf_state);
+
+ /* we flush this buffer when switching to PERMANENT */
+ if ((buf_state & (BM_VALID | BM_DIRTY)) == (BM_VALID | BM_DIRTY))
+ {
+ PinBuffer_Locked(bufHdr);
+ LWLockAcquire(BufferDescriptorGetContentLock(bufHdr),
+ LW_SHARED);
+ FlushBuffer(bufHdr, srel);
+ LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
+ UnpinBuffer(bufHdr, true);
+ }
+ else
+ UnlockBufHdr(bufHdr, buf_state);
+ }
+ else
+ {
+ /* init fork is always BM_PERMANENT. See BufferAlloc */
+ if (bufHdr->tag.forkNum != INIT_FORKNUM)
+ buf_state &= ~BM_PERMANENT;
+
+ UnlockBufHdr(bufHdr, buf_state);
+ }
+ }
+}
+
/* ---------------------------------------------------------------------
* DropRelFileNodesAllBuffers
*
diff --git a/src/backend/storage/file/reinit.c b/src/backend/storage/file/reinit.c
index 40c758d789..0eac1956cc 100644
--- a/src/backend/storage/file/reinit.c
+++ b/src/backend/storage/file/reinit.c
@@ -16,29 +16,50 @@
#include <unistd.h>
+#include "access/xlog.h"
+#include "catalog/pg_tablespace_d.h"
#include "common/relpath.h"
+#include "storage/bufmgr.h"
#include "storage/copydir.h"
#include "storage/fd.h"
+#include "storage/md.h"
#include "storage/reinit.h"
+#include "storage/smgr.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
static void ResetUnloggedRelationsInTablespaceDir(const char *tsdirname,
- int op);
+ Oid tspid, int op);
static void ResetUnloggedRelationsInDbspaceDir(const char *dbspacedirname,
- int op);
+ Oid tspid, Oid dbid, int op);
typedef struct
{
Oid reloid; /* hash key */
-} unlogged_relation_entry;
+ bool has_init; /* has INIT fork */
+ bool dirty_init; /* needs to remove INIT fork */
+ bool dirty_all; /* needs to remove all forks */
+} relfile_entry;
/*
- * Reset unlogged relations from before the last restart.
+ * Clean up and reset relation files from before the last restart.
*
- * If op includes UNLOGGED_RELATION_CLEANUP, we remove all forks of any
- * relation with an "init" fork, except for the "init" fork itself.
+ * If op includes UNLOGGED_RELATION_CLEANUP, we perform different operations
+ * depending on the existence of the "cleanup" forks.
*
+ * If CLEANUP_FORKNUM (clup) is present, we remove the init fork of the same
+ * relation along with the clup fork.
+ *
+ * If CLEANUP2_FORKNUM (cln2) is present we remove the whole relation along
+ * with the cln2 fork.
+ *
+ * Otherwise, if the "init" fork is found. we remove all forks of any relation
+ * with the "init" fork, except for the "init" fork itself.
+ *
+ *
+ * If op includes UNLOGGED_RELATION_DROP_BUFFER, we drop all buffers for all
+ * relations that have the "cleanup" and/or the "init" forks.
+ *
* If op includes UNLOGGED_RELATION_INIT, we copy the "init" fork to the main
* fork.
*/
@@ -68,7 +89,7 @@ ResetUnloggedRelations(int op)
/*
* First process unlogged files in pg_default ($PGDATA/base)
*/
- ResetUnloggedRelationsInTablespaceDir("base", op);
+ ResetUnloggedRelationsInTablespaceDir("base", DEFAULTTABLESPACE_OID, op);
/*
* Cycle through directories for all non-default tablespaces.
@@ -77,13 +98,19 @@ ResetUnloggedRelations(int op)
while ((spc_de = ReadDir(spc_dir, "pg_tblspc")) != NULL)
{
+ Oid tspid;
+
if (strcmp(spc_de->d_name, ".") == 0 ||
strcmp(spc_de->d_name, "..") == 0)
continue;
snprintf(temp_path, sizeof(temp_path), "pg_tblspc/%s/%s",
spc_de->d_name, TABLESPACE_VERSION_DIRECTORY);
- ResetUnloggedRelationsInTablespaceDir(temp_path, op);
+
+ tspid = atooid(spc_de->d_name);
+ Assert(tspid != 0);
+
+ ResetUnloggedRelationsInTablespaceDir(temp_path, tspid, op);
}
FreeDir(spc_dir);
@@ -99,7 +126,8 @@ ResetUnloggedRelations(int op)
* Process one tablespace directory for ResetUnloggedRelations
*/
static void
-ResetUnloggedRelationsInTablespaceDir(const char *tsdirname, int op)
+ResetUnloggedRelationsInTablespaceDir(const char *tsdirname,
+ Oid tspid, int op)
{
DIR *ts_dir;
struct dirent *de;
@@ -126,6 +154,8 @@ ResetUnloggedRelationsInTablespaceDir(const char *tsdirname, int op)
while ((de = ReadDir(ts_dir, tsdirname)) != NULL)
{
+ Oid dbid;
+
/*
* We're only interested in the per-database directories, which have
* numeric names. Note that this code will also (properly) ignore "."
@@ -136,7 +166,10 @@ ResetUnloggedRelationsInTablespaceDir(const char *tsdirname, int op)
snprintf(dbspace_path, sizeof(dbspace_path), "%s/%s",
tsdirname, de->d_name);
- ResetUnloggedRelationsInDbspaceDir(dbspace_path, op);
+ dbid = atooid(de->d_name);
+ Assert(dbid != 0);
+
+ ResetUnloggedRelationsInDbspaceDir(dbspace_path, tspid, dbid, op);
}
FreeDir(ts_dir);
@@ -146,125 +179,226 @@ ResetUnloggedRelationsInTablespaceDir(const char *tsdirname, int op)
* Process one per-dbspace directory for ResetUnloggedRelations
*/
static void
-ResetUnloggedRelationsInDbspaceDir(const char *dbspacedirname, int op)
+ResetUnloggedRelationsInDbspaceDir(const char *dbspacedirname,
+ Oid tspid, Oid dbid, int op)
{
DIR *dbspace_dir;
struct dirent *de;
char rm_path[MAXPGPATH * 2];
+ HTAB *hash;
+ HASHCTL ctl;
/* Caller must specify at least one operation. */
- Assert((op & (UNLOGGED_RELATION_CLEANUP | UNLOGGED_RELATION_INIT)) != 0);
+ Assert((op & (UNLOGGED_RELATION_CLEANUP |
+ UNLOGGED_RELATION_DROP_BUFFER |
+ UNLOGGED_RELATION_INIT)) != 0);
/*
* Cleanup is a two-pass operation. First, we go through and identify all
* the files with init forks. Then, we go through again and nuke
* everything with the same OID except the init fork.
*/
+
+ /*
+ * It's possible that someone could create a ton of unlogged relations
+ * in the same database & tablespace, so we'd better use a hash table
+ * rather than an array or linked list to keep track of which files
+ * need to be reset. Otherwise, this cleanup operation would be
+ * O(n^2).
+ */
+ memset(&ctl, 0, sizeof(ctl));
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(relfile_entry);
+ hash = hash_create("relfilenode cleanup hash",
+ 32, &ctl, HASH_ELEM | HASH_BLOBS);
+
+ /* Collect INIT and CLEANUP forks in the directory. */
+ dbspace_dir = AllocateDir(dbspacedirname);
+ while ((de = ReadDir(dbspace_dir, dbspacedirname)) != NULL)
+ {
+ int oidchars;
+ ForkNumber forkNum;
+
+ /* Skip anything that doesn't look like a relation data file. */
+ if (!parse_filename_for_nontemp_relation(de->d_name, &oidchars,
+ &forkNum))
+ continue;
+
+ if (forkNum == INIT_FORKNUM ||
+ forkNum == CLEANUP_FORKNUM || forkNum == CLEANUP2_FORKNUM)
+ {
+ Oid key;
+ relfile_entry *ent;
+ bool found;
+
+ /*
+ * Record the relfilenode information. If it has the CLEANUP fork,
+ * the relfilenode is in dirty state, where clean up is needed.
+ */
+ key = atooid(de->d_name);
+ ent = hash_search(hash, &key, HASH_ENTER, &found);
+
+ if (!found)
+ {
+ ent->has_init = false;
+ ent->dirty_init = false;
+ ent->dirty_all = false;
+ }
+
+ if (forkNum == CLEANUP_FORKNUM)
+ ent->dirty_init = true;
+ else if (forkNum == CLEANUP2_FORKNUM)
+ ent->dirty_all = true;
+ else
+ {
+ Assert(forkNum == INIT_FORKNUM);
+ ent->has_init = true;
+ }
+ }
+ }
+
+ /* Done with the first pass. */
+ FreeDir(dbspace_dir);
+
+ /* nothing to do if we don't have init nor cleanup forks */
+ if (hash_get_num_entries(hash) < 1)
+ {
+ hash_destroy(hash);
+ return;
+ }
+
+ if ((op & UNLOGGED_RELATION_DROP_BUFFER) != 0)
+ {
+ /*
+ * When we come here after recovery, smgr object for this file might
+ * have been created. In that case we need to drop all buffers then the
+ * smgr object before initializing the unlogged relation. This is safe
+ * as far as no other backends have accessed the relation before
+ * starting archive recovery.
+ */
+ HASH_SEQ_STATUS status;
+ relfile_entry *ent;
+ SMgrRelation *srels = palloc(sizeof(SMgrRelation) * 8);
+ int maxrels = 8;
+ int nrels = 0;
+ int i;
+
+ Assert(!HotStandbyActive());
+
+ hash_seq_init(&status, hash);
+ while((ent = (relfile_entry *) hash_seq_search(&status)) != NULL)
+ {
+ RelFileNodeBackend rel;
+
+ /*
+ * The relation is persistent and stays remain persistent. Don't
+ * drop the buffers for this relation.
+ */
+ if (ent->has_init && ent->dirty_init)
+ continue;
+
+ if (maxrels <= nrels)
+ {
+ maxrels *= 2;
+ srels = repalloc(srels, sizeof(SMgrRelation) * maxrels);
+ }
+
+ rel.backend = InvalidBackendId;
+ rel.node.spcNode = tspid;
+ rel.node.dbNode = dbid;
+ rel.node.relNode = ent->reloid;
+
+ srels[nrels++] = smgropen(rel.node, InvalidBackendId);
+ }
+
+ DropRelFileNodesAllBuffers(srels, nrels);
+
+ for (i = 0 ; i < nrels ; i++)
+ smgrclose(srels[i]);
+ }
+
+ /*
+ * Now, make a second pass and remove anything that matches.
+ */
if ((op & UNLOGGED_RELATION_CLEANUP) != 0)
{
- HTAB *hash;
- HASHCTL ctl;
-
- /*
- * It's possible that someone could create a ton of unlogged relations
- * in the same database & tablespace, so we'd better use a hash table
- * rather than an array or linked list to keep track of which files
- * need to be reset. Otherwise, this cleanup operation would be
- * O(n^2).
- */
- ctl.keysize = sizeof(Oid);
- ctl.entrysize = sizeof(unlogged_relation_entry);
- ctl.hcxt = CurrentMemoryContext;
- hash = hash_create("unlogged relation OIDs", 32, &ctl,
- HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
-
- /* Scan the directory. */
- dbspace_dir = AllocateDir(dbspacedirname);
- while ((de = ReadDir(dbspace_dir, dbspacedirname)) != NULL)
- {
- ForkNumber forkNum;
- int oidchars;
- unlogged_relation_entry ent;
-
- /* Skip anything that doesn't look like a relation data file. */
- if (!parse_filename_for_nontemp_relation(de->d_name, &oidchars,
- &forkNum))
- continue;
-
- /* Also skip it unless this is the init fork. */
- if (forkNum != INIT_FORKNUM)
- continue;
-
- /*
- * Put the OID portion of the name into the hash table, if it
- * isn't already.
- */
- ent.reloid = atooid(de->d_name);
- (void) hash_search(hash, &ent, HASH_ENTER, NULL);
- }
-
- /* Done with the first pass. */
- FreeDir(dbspace_dir);
-
- /*
- * If we didn't find any init forks, there's no point in continuing;
- * we can bail out now.
- */
- if (hash_get_num_entries(hash) == 0)
- {
- hash_destroy(hash);
- return;
- }
-
- /*
- * Now, make a second pass and remove anything that matches.
- */
dbspace_dir = AllocateDir(dbspacedirname);
while ((de = ReadDir(dbspace_dir, dbspacedirname)) != NULL)
{
- ForkNumber forkNum;
- int oidchars;
- unlogged_relation_entry ent;
+ ForkNumber forkNum;
+ int oidchars;
+ Oid key;
+ relfile_entry *ent;
+ RelFileNodeBackend rel;
/* Skip anything that doesn't look like a relation data file. */
if (!parse_filename_for_nontemp_relation(de->d_name, &oidchars,
&forkNum))
continue;
- /* We never remove the init fork. */
- if (forkNum == INIT_FORKNUM)
- continue;
-
/*
* See whether the OID portion of the name shows up in the hash
* table. If so, nuke it!
*/
- ent.reloid = atooid(de->d_name);
- if (hash_search(hash, &ent, HASH_FIND, NULL))
+ key = atooid(de->d_name);
+ ent = hash_search(hash, &key, HASH_FIND, NULL);
+
+ if (!ent)
+ continue;
+
+ if (!ent->dirty_all)
{
- snprintf(rm_path, sizeof(rm_path), "%s/%s",
- dbspacedirname, de->d_name);
- if (unlink(rm_path) < 0)
- ereport(ERROR,
- (errcode_for_file_access(),
- errmsg("could not remove file \"%s\": %m",
- rm_path)));
+ /* clean permanent relations don't need cleanup */
+ if (!ent->has_init)
+ continue;
+
+ if (ent->dirty_init)
+ {
+ /*
+ * The crashed trasaction did SET UNLOGGED. This relation
+ * is restored to a LOGGED relation.
+ */
+ if (forkNum != INIT_FORKNUM && forkNum != CLEANUP_FORKNUM)
+ continue;
+ }
else
- elog(DEBUG2, "unlinked file \"%s\"", rm_path);
+ {
+ /*
+ * we don't remove the INIT fork of a non-dirty
+ * relfilenode
+ */
+ if (forkNum == INIT_FORKNUM)
+ continue;
+ }
}
+
+ /* so, nuke it! */
+ snprintf(rm_path, sizeof(rm_path), "%s/%s",
+ dbspacedirname, de->d_name);
+ if (unlink(rm_path) < 0)
+ ereport(ERROR,
+ (errcode_for_file_access(),
+ errmsg("could not remove file \"%s\": %m",
+ rm_path)));
+
+ rel.backend = InvalidBackendId;
+ rel.node.spcNode = tspid;
+ rel.node.dbNode = dbid;
+ rel.node.relNode = atooid(de->d_name);
+
+ ForgetRelationForkSyncRequests(rel, forkNum);
}
/* Cleanup is complete. */
FreeDir(dbspace_dir);
- hash_destroy(hash);
}
+ hash_destroy(hash);
+ hash = NULL;
+
/*
* Initialization happens after cleanup is complete: we copy each init
- * fork file to the corresponding main fork file. Note that if we are
- * asked to do both cleanup and init, we may never get here: if the
- * cleanup code determines that there are no init forks in this dbspace,
- * it will return before we get to this point.
+ * fork file to the corresponding main fork file.
*/
if ((op & UNLOGGED_RELATION_INIT) != 0)
{
diff --git a/src/backend/storage/smgr/md.c b/src/backend/storage/smgr/md.c
index 0643d714fb..6b37195c52 100644
--- a/src/backend/storage/smgr/md.c
+++ b/src/backend/storage/smgr/md.c
@@ -338,8 +338,10 @@ mdunlinkfork(RelFileNodeBackend rnode, ForkNumber forkNum, bool isRedo)
if (ret == 0 || errno != ENOENT)
{
ret = unlink(path);
+
+ /* failure of removing cleanup fork leads to a data loss. */
if (ret < 0 && errno != ENOENT)
- ereport(WARNING,
+ ereport((forkNum != CLEANUP_FORKNUM ? WARNING : ERROR),
(errcode_for_file_access(),
errmsg("could not remove file \"%s\": %m", path)));
}
@@ -1024,6 +1026,15 @@ register_forget_request(RelFileNodeBackend rnode, ForkNumber forknum,
RegisterSyncRequest(&tag, SYNC_FORGET_REQUEST, true /* retryOnError */ );
}
+/*
+ * ForgetRelationForkSyncRequests -- forget any fsyncs and unlinks for a fork
+ */
+void
+ForgetRelationForkSyncRequests(RelFileNodeBackend rnode, ForkNumber forknum)
+{
+ register_forget_request(rnode, forknum, 0);
+}
+
/*
* ForgetDatabaseSyncRequests -- forget any fsyncs and unlinks for a DB
*/
diff --git a/src/backend/storage/smgr/smgr.c b/src/backend/storage/smgr/smgr.c
index 4dc24649df..96480e321d 100644
--- a/src/backend/storage/smgr/smgr.c
+++ b/src/backend/storage/smgr/smgr.c
@@ -662,6 +662,12 @@ smgrimmedsync(SMgrRelation reln, ForkNumber forknum)
smgrsw[reln->smgr_which].smgr_immedsync(reln, forknum);
}
+void
+smgrunlink(SMgrRelation reln, ForkNumber forknum, bool isRedo)
+{
+ smgrsw[reln->smgr_which].smgr_unlink(reln->smgr_rnode, forknum, isRedo);
+}
+
/*
* AtEOXact_SMgr
*
diff --git a/src/common/relpath.c b/src/common/relpath.c
index 1f5c426ec0..479dcc248e 100644
--- a/src/common/relpath.c
+++ b/src/common/relpath.c
@@ -34,7 +34,9 @@ const char *const forkNames[] = {
"main", /* MAIN_FORKNUM */
"fsm", /* FSM_FORKNUM */
"vm", /* VISIBILITYMAP_FORKNUM */
- "init" /* INIT_FORKNUM */
+ "init", /* INIT_FORKNUM */
+ "clup", /* CLEANUP_FORKNUM */
+ "cln2" /* CLEANUP2_FORKNUM */
};
StaticAssertDecl(lengthof(forkNames) == (MAX_FORKNUM + 1),
diff --git a/src/include/catalog/storage.h b/src/include/catalog/storage.h
index 0ab32b44e9..382623159c 100644
--- a/src/include/catalog/storage.h
+++ b/src/include/catalog/storage.h
@@ -23,6 +23,8 @@
extern int wal_skip_threshold;
extern SMgrRelation RelationCreateStorage(RelFileNode rnode, char relpersistence);
+extern void RelationCreateInitFork(Relation rel);
+extern void RelationDropInitFork(Relation rel);
extern void RelationDropStorage(Relation rel);
extern void RelationPreserveStorage(RelFileNode rnode, bool atCommit);
extern void RelationPreTruncate(Relation rel);
diff --git a/src/include/catalog/storage_xlog.h b/src/include/catalog/storage_xlog.h
index f0814f1458..0fd0832a8b 100644
--- a/src/include/catalog/storage_xlog.h
+++ b/src/include/catalog/storage_xlog.h
@@ -22,13 +22,17 @@
/*
* Declarations for smgr-related XLOG records
*
- * Note: we log file creation and truncation here, but logging of deletion
- * actions is handled by xact.c, because it is part of transaction commit.
+ * Note: we log file creation, truncation, deletion and persistence change
+ * here. logging of deletion actions is mainly handled by xact.c, because it is
+ * part of transaction commit, but we log deletions happens outside of a
+ * transaction.
*/
/* XLOG gives us high 4 bits */
#define XLOG_SMGR_CREATE 0x10
#define XLOG_SMGR_TRUNCATE 0x20
+#define XLOG_SMGR_UNLINK 0x30
+#define XLOG_SMGR_BUFPERSISTENCE 0x40
typedef struct xl_smgr_create
{
@@ -36,6 +40,18 @@ typedef struct xl_smgr_create
ForkNumber forkNum;
} xl_smgr_create;
+typedef struct xl_smgr_unlink
+{
+ RelFileNode rnode;
+ ForkNumber forkNum;
+} xl_smgr_unlink;
+
+typedef struct xl_smgr_bufpersistence
+{
+ RelFileNode rnode;
+ bool persistence;
+} xl_smgr_bufpersistence;
+
/* flags for xl_smgr_truncate */
#define SMGR_TRUNCATE_HEAP 0x0001
#define SMGR_TRUNCATE_VM 0x0002
@@ -51,6 +67,8 @@ typedef struct xl_smgr_truncate
} xl_smgr_truncate;
extern void log_smgrcreate(const RelFileNode *rnode, ForkNumber forkNum);
+extern void log_smgrunlink(const RelFileNode *rnode, ForkNumber forkNum);
+extern void log_smgrbufpersistence(const RelFileNode *rnode, bool persistence);
extern void smgr_redo(XLogReaderState *record);
extern void smgr_desc(StringInfo buf, XLogReaderState *record);
diff --git a/src/include/common/relpath.h b/src/include/common/relpath.h
index a44be11ca0..040070aa2b 100644
--- a/src/include/common/relpath.h
+++ b/src/include/common/relpath.h
@@ -43,7 +43,9 @@ typedef enum ForkNumber
MAIN_FORKNUM = 0,
FSM_FORKNUM,
VISIBILITYMAP_FORKNUM,
- INIT_FORKNUM
+ INIT_FORKNUM,
+ CLEANUP_FORKNUM,
+ CLEANUP2_FORKNUM
/*
* NOTE: if you add a new fork, change MAX_FORKNUM and possibly
@@ -52,7 +54,7 @@ typedef enum ForkNumber
*/
} ForkNumber;
-#define MAX_FORKNUM INIT_FORKNUM
+#define MAX_FORKNUM CLEANUP2_FORKNUM
#define FORKNAMECHARS 4 /* max chars for a fork name */
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index fb00fda6a7..ccb0a388f6 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -205,6 +205,8 @@ extern void FlushRelationsAllBuffers(struct SMgrRelationData **smgrs, int nrels)
extern void FlushDatabaseBuffers(Oid dbid);
extern void DropRelFileNodeBuffers(struct SMgrRelationData *smgr_reln, ForkNumber *forkNum,
int nforks, BlockNumber *firstDelBlock);
+extern void SetRelationBuffersPersistence(struct SMgrRelationData *srel,
+ bool permanent, bool isRedo);
extern void DropRelFileNodesAllBuffers(struct SMgrRelationData **smgr_reln, int nnodes);
extern void DropDatabaseBuffers(Oid dbid);
diff --git a/src/include/storage/md.h b/src/include/storage/md.h
index 752b440864..3cbbbf2edd 100644
--- a/src/include/storage/md.h
+++ b/src/include/storage/md.h
@@ -41,6 +41,8 @@ extern void mdtruncate(SMgrRelation reln, ForkNumber forknum,
BlockNumber nblocks);
extern void mdimmedsync(SMgrRelation reln, ForkNumber forknum);
+extern void ForgetRelationForkSyncRequests(RelFileNodeBackend rnode,
+ ForkNumber forknum);
extern void ForgetDatabaseSyncRequests(Oid dbid);
extern void DropRelationFiles(RelFileNode *delrels, int ndelrels, bool isRedo);
diff --git a/src/include/storage/reinit.h b/src/include/storage/reinit.h
index fad1e5c473..b969ba8e86 100644
--- a/src/include/storage/reinit.h
+++ b/src/include/storage/reinit.h
@@ -23,6 +23,7 @@ extern bool parse_filename_for_nontemp_relation(const char *name,
int *oidchars, ForkNumber *fork);
#define UNLOGGED_RELATION_CLEANUP 0x0001
-#define UNLOGGED_RELATION_INIT 0x0002
+#define UNLOGGED_RELATION_DROP_BUFFER 0x0002
+#define UNLOGGED_RELATION_INIT 0x0004
#endif /* REINIT_H */
diff --git a/src/include/storage/smgr.h b/src/include/storage/smgr.h
index a6fbf7b6a6..1ac3e4a74a 100644
--- a/src/include/storage/smgr.h
+++ b/src/include/storage/smgr.h
@@ -86,6 +86,7 @@ extern void smgrclose(SMgrRelation reln);
extern void smgrcloseall(void);
extern void smgrclosenode(RelFileNodeBackend rnode);
extern void smgrcreate(SMgrRelation reln, ForkNumber forknum, bool isRedo);
+extern void smgrunlink(SMgrRelation reln, ForkNumber forknum, bool isRedo);
extern void smgrdosyncall(SMgrRelation *rels, int nrels);
extern void smgrdounlinkall(SMgrRelation *rels, int nrels, bool isRedo);
extern void smgrextend(SMgrRelation reln, ForkNumber forknum,
--
2.27.0
----Next_Part(Thu_Jan_14_17_32_17_2021_289)--
Content-Type: Text/X-Patch; charset=us-ascii
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="v5-0002-New-command-ALTER-TABLE-ALL-IN-TABLESPACE-SET-LOG.patch"
^ permalink raw reply [nested|flat] 14+ messages in thread
* Function scan FDW pushdown
@ 2021-05-20 17:43 Alexander Pyhalov <[email protected]>
2021-06-15 13:15 ` Re: Function scan FDW pushdown Ashutosh Bapat <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Pyhalov @ 2021-05-20 17:43 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
Hi.
The attached patch allows pushing joins with function RTEs to PostgreSQL
data sources.
This makes executing queries like this
create foreign table f_pgbench_accounts (aid int, bid int, abalance int,
filler char(84)) SERVER local_srv OPTIONS (table_name
'pgbench_accounts');
select * from f_pgbench_accounts join unnest(array[1,2,3]) ON unnest =
aid;
more efficient.
with patch:
# explain analyze select * from f_pgbench_accounts join
unnest(array[1,2,3,4,5,6]) ON unnest = aid;
QUERY PLAN
------------------------------------------------------------------------------------------------
Foreign Scan (cost=100.00..116.95 rows=7 width=356) (actual
time=2.282..2.287 rows=6 loops=1)
Relations: (f_pgbench_accounts) INNER JOIN (FUNCTION RTE unnest)
Planning Time: 0.487 ms
Execution Time: 3.336 ms
without patch:
# explain analyze select * from f_pgbench_accounts join
unnest(array[1,2,3,4,5,6]) ON unnest = aid;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=100.14..158.76 rows=7 width=356) (actual
time=2.263..1268.607 rows=6 loops=1)
Hash Cond: (f_pgbench_accounts.aid = unnest.unnest)
-> Foreign Scan on f_pgbench_accounts (cost=100.00..157.74 rows=217
width=352) (actual time=2.190..1205.938 rows=100000 loops=1)
-> Hash (cost=0.06..0.06 rows=6 width=4) (actual time=0.041..0.043
rows=6 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
-> Function Scan on unnest (cost=0.00..0.06 rows=6 width=4)
(actual time=0.025..0.028 rows=6 loops=1)
Planning Time: 0.389 ms
Execution Time: 1269.627 ms
So far I don't know how to visualize actual function expression used in
function RTE, as in postgresExplainForeignScan() es->rtable comes from
queryDesc->plannedstmt->rtable, and rte->functions is already 0.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] 0001-Function-scan-FDW-pushdown.patch (26.8K, ../../[email protected]/2-0001-Function-scan-FDW-pushdown.patch)
download | inline diff:
From 6b5ea4c62a1fcd3dad586d4f461cb142834ac266 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 17 May 2021 19:19:31 +0300
Subject: [PATCH] Function scan FDW pushdown
---
contrib/postgres_fdw/deparse.c | 82 ++++--
.../postgres_fdw/expected/postgres_fdw.out | 123 ++++++++
contrib/postgres_fdw/postgres_fdw.c | 273 ++++++++++++++++--
contrib/postgres_fdw/sql/postgres_fdw.sql | 60 ++++
src/backend/optimizer/util/relnode.c | 58 +++-
5 files changed, 547 insertions(+), 49 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 31919fda8c6..292ba52ea14 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -1613,13 +1613,36 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
{
RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
- /*
- * Core code already has some lock on each rel being planned, so we
- * can use NoLock here.
- */
- Relation rel = table_open(rte->relid, NoLock);
+ Assert(rte->rtekind == RTE_RELATION || rte->rtekind == RTE_FUNCTION);
+ if (rte->rtekind == RTE_RELATION)
+ {
+ /*
+ * Core code already has some lock on each rel being planned, so
+ * we can use NoLock here.
+ */
+ Relation rel = table_open(rte->relid, NoLock);
- deparseRelation(buf, rel);
+ deparseRelation(buf, rel);
+
+ table_close(rel, NoLock);
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ RangeTblFunction *rtfunc;
+ deparse_expr_cxt context;
+
+ Assert(list_length(rte->functions) == 1);
+
+ rtfunc = (RangeTblFunction *) linitial(rte->functions);
+
+ context.root = root;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.buf = buf;
+ context.params_list = params_list;
+
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ }
/*
* Add a unique alias to avoid any conflict in relation names due to
@@ -1628,8 +1651,6 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
*/
if (use_alias)
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
-
- table_close(rel, NoLock);
}
}
@@ -2309,29 +2330,40 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
* If it's a column of a foreign table, and it has the column_name FDW
* option, use that value.
*/
- options = GetForeignColumnOptions(rte->relid, varattno);
- foreach(lc, options)
+ if (rte->rtekind == RTE_RELATION)
{
- DefElem *def = (DefElem *) lfirst(lc);
-
- if (strcmp(def->defname, "column_name") == 0)
+ options = GetForeignColumnOptions(rte->relid, varattno);
+ foreach(lc, options)
{
- colname = defGetString(def);
- break;
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "column_name") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
}
- }
- /*
- * If it's a column of a regular table or it doesn't have column_name
- * FDW option, use attribute name.
- */
- if (colname == NULL)
- colname = get_attname(rte->relid, varattno, false);
+ /*
+ * If it's a column of a regular table or it doesn't have
+ * column_name FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, varattno, false);
- if (qualify_col)
- ADD_REL_QUALIFIER(buf, varno);
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
- appendStringInfoString(buf, quote_identifier(colname));
+ appendStringInfoString(buf, quote_identifier(colname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * If it's a column of a function rte, use not column name, but
+ * RTE alias
+ */
+ appendStringInfo((buf), "%s%d", REL_ALIAS_PREFIX, (varno));
+ }
}
}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7df30010f25..da3abd453c2 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -10231,3 +10231,126 @@ DROP TABLE result_tbl;
DROP TABLE join_tbl;
ALTER SERVER loopback OPTIONS (DROP async_capable);
ALTER SERVER loopback2 OPTIONS (DROP async_capable);
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: (public.remote_tbl r) INNER JOIN (FUNCTION RTE n)
+ Remote SQL: SELECT r1.a, r1.b, r2 FROM (public.base_tbl r1 INNER JOIN unnest('{2,3,4}'::integer[]) r2 ON (((r1.a = r2))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+ a | b | n
+---+---+---
+ 2 | 4 | 2
+ 3 | 6 | 3
+ 4 | 8 | 4
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (FUNCTION RTE n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT r1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) r1 INNER JOIN public.base_tbl r2 ON (((r1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+ n | a | b
+---+---+---
+ 2 | 2 | 4
+ 3 | 3 | 6
+ 4 | 4 | 8
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, r1.c, r1.d, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (FUNCTION RTE n)
+ Remote SQL: SELECT r1.a, r1.b, r2.c, r2.d, r3 FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 ON (((r1.a = r3))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = r1.c AND r1.c = n
+ORDER BY r.a;
+ a | b | c | d | n
+---+---+---+-------+---
+ 3 | 6 | 3 | text3 | 3
+ 4 | 8 | 4 | text4 | 4
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (FUNCTION RTE n)
+ Remote SQL: SELECT r1.a, r1.b, r3 FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 ON (((r1.a = r3)) AND ((r3 > 3))))
+(4 rows)
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: r.a, r.b, (generate_series(1, 10))
+ Hash Cond: (r.a = (generate_series(1, 10)))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl
+ -> Hash
+ Output: (generate_series(1, 10))
+ -> ProjectSet
+ Output: generate_series(1, 10)
+ -> Result
+(11 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+DROP FUNCTION f(INTEGER);
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index c48a421e88b..cf2db2379ed 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -547,6 +547,11 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
const PgFdwRelationInfo *fpinfo_i);
static int get_batch_size_option(Relation rel);
+static bool is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel);
+static void initialize_nonrel_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ PgFdwRelationInfo *existing_fpinfo);
+
/*
* Foreign-data wrapper handler function: return a struct with pointers
@@ -787,6 +792,142 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->relation_index = baserel->relid;
}
+/*
+ * initialize_nonrel_fpinfo
+ * Initialize fpinfo for non-refering tables queries
+ */
+static void
+initialize_nonrel_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ PgFdwRelationInfo *existing_fpinfo)
+{
+ PgFdwRelationInfo *fpinfo;
+ ListCell *lc;
+
+ Assert(existing_fpinfo);
+
+ /*
+ * We use PgFdwRelationInfo to pass various information to subsequent
+ * functions.
+ */
+ fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ baserel->fdw_private = (void *) fpinfo;
+
+ /* Base foreign tables need to be pushed down always. */
+ fpinfo->pushdown_safe = true;
+
+ /* We don't have any table, related to query */
+ fpinfo->table = NULL;
+ fpinfo->server = GetForeignServer(existing_fpinfo->server->serverid);
+
+ /*
+ * Extract user-settable option values. Note that per-table settings of
+ * use_remote_estimate, fetch_size and async_capable override per-server
+ * settings of them, respectively.
+ */
+ merge_fdw_options(fpinfo, existing_fpinfo, NULL);
+
+ /*
+ * If the table or the server is configured to use remote estimates,
+ * identify which user to do remote access as during planning. This
+ * should match what ExecCheckRTEPerms() does. If we fail due to lack of
+ * permissions, the query would have failed at runtime anyway.
+ */
+ fpinfo->user = existing_fpinfo->user;
+
+ /*
+ * Identify which baserestrictinfo clauses can be sent to the remote
+ * server and which can't.
+ */
+ classifyConditions(root, baserel, baserel->baserestrictinfo,
+ &fpinfo->remote_conds, &fpinfo->local_conds);
+
+ /*
+ * Identify which attributes will need to be retrieved from the remote
+ * server. These include all attrs needed for joins or final output, plus
+ * all attrs used in the local_conds. (Note: if we end up using a
+ * parameterized scan, it's possible that some of the join clauses will be
+ * sent to the remote and thus we wouldn't really need to retrieve the
+ * columns used in them. Doesn't seem worth detecting that case though.)
+ */
+ fpinfo->attrs_used = NULL;
+ pull_varattnos((Node *) baserel->reltarget->exprs, baserel->relid,
+ &fpinfo->attrs_used);
+ foreach(lc, fpinfo->local_conds)
+ {
+ RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
+
+ pull_varattnos((Node *) rinfo->clause, baserel->relid,
+ &fpinfo->attrs_used);
+ }
+
+ /*
+ * Compute the selectivity and cost of the local_conds, so we don't have
+ * to do it over again for each path. The best we can do for these
+ * conditions is to estimate selectivity on the basis of local statistics.
+ */
+ fpinfo->local_conds_sel = clauselist_selectivity(root,
+ fpinfo->local_conds,
+ baserel->relid,
+ JOIN_INNER,
+ NULL);
+
+ cost_qual_eval(&fpinfo->local_conds_cost, fpinfo->local_conds, root);
+
+ /*
+ * Set # of retrieved rows and cached relation costs to some negative
+ * value, so that we can detect when they are set to some sensible values,
+ * during one (usually the first) of the calls to estimate_path_cost_size.
+ */
+ fpinfo->retrieved_rows = -1;
+ fpinfo->rel_startup_cost = -1;
+ fpinfo->rel_total_cost = -1;
+
+ /*
+ * Don't try to execute anything on remote server for non-relation-based
+ * query
+ */
+ fpinfo->use_remote_estimate = false;
+
+ /*
+ * If the foreign table has never been ANALYZEd, it will have reltuples <
+ * 0, meaning "unknown". We can't do much if we're not allowed to consult
+ * the remote server, but we can use a hack similar to plancat.c's
+ * treatment of empty relations: use a minimum size estimate of 10 pages,
+ * and divide by the column-datatype-based width estimate to get the
+ * corresponding number of tuples.
+ */
+ if (baserel->tuples < 0)
+ {
+ baserel->pages = 10;
+ baserel->tuples =
+ (10 * BLCKSZ) / (baserel->reltarget->width +
+ MAXALIGN(SizeofHeapTupleHeader));
+ }
+
+ /* Estimate baserel size as best we can with local statistics. */
+ set_baserel_size_estimates(root, baserel);
+
+ /* Fill in basically-bogus cost estimates for use later. */
+ estimate_path_cost_size(root, baserel, NIL, NIL, NULL,
+ &fpinfo->rows, &fpinfo->width,
+ &fpinfo->startup_cost, &fpinfo->total_cost);
+
+ /*
+ * fpinfo->relation_name gets the numeric rangetable index of the foreign
+ * table RTE. (If this query gets EXPLAIN'd, we'll convert that to a
+ * human-readable string at that time.)
+ */
+ fpinfo->relation_name = psprintf("%u", baserel->relid);
+
+ /* No outer and inner relations. */
+ fpinfo->make_outerrel_subquery = false;
+ fpinfo->make_innerrel_subquery = false;
+ fpinfo->lower_subquery_rels = NULL;
+ /* Set the relation index. */
+ fpinfo->relation_index = baserel->relid;
+}
+
/*
* get_useful_ecs_for_relation
* Determine which EquivalenceClasses might be involved in useful
@@ -1470,15 +1611,25 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckRTEPerms() does. In case of a join or aggregate, use the
- * lowest-numbered member RTE as a representative; we would get the same
- * result from any.
+ * ExecCheckRTEPerms() does. In case of a join or aggregate, scan RTEs
+ * until RTE_RELATION is found. We would get the same result from any.
*/
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte && rte->rtekind == RTE_RELATION)
+ break;
+ }
+ Assert(rte && rte->rtekind == RTE_RELATION);
+ }
userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
/* Get info about foreign table. */
@@ -2795,21 +2946,57 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_relids));
rte = rt_fetch(rti, es->rtable);
- Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
- relname = get_rel_name(rte->relid);
- if (es->verbose)
+ if (rte->rtekind == RTE_RELATION)
{
- char *namespace;
-
- namespace = get_namespace_name(get_rel_namespace(rte->relid));
- appendStringInfo(relations, "%s.%s",
- quote_identifier(namespace),
- quote_identifier(relname));
+ relname = get_rel_name(rte->relid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name(get_rel_namespace(rte->relid));
+ appendStringInfo(relations, "%s.%s",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfoString(relations,
+ quote_identifier(relname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ appendStringInfoString(relations, "FUNCTION RTE");
+#if 0
+
+ /*
+ * TODO: rte->functions is always 0, how should we proceed
+ * here ?
+ */
+ if (list_length(rte->functions) == 1)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) linitial(rte->functions);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ {
+ FuncExpr *funcexpr = (FuncExpr *) rtfunc->funcexpr;
+ Oid funcid = funcexpr->funcid;
+
+ relname = get_func_name(funcid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name(get_func_namespace(funcid));
+ appendStringInfo(relations, "%s.%s()",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfo(relations, "%s()", quote_identifier(relname));
+ }
+ }
+#endif
}
- else
- appendStringInfoString(relations,
- quote_identifier(relname));
refname = (char *) list_nth(es->rtable_names, rti - 1);
if (refname == NULL)
refname = rte->eref->aliasname;
@@ -5405,6 +5592,27 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Determine if foreignrel, not backed by foreign
+ * table, is fine to push down.
+ */
+static bool
+is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel)
+{
+ RangeTblEntry *rte;
+ RangeTblFunction *rtfunc;
+
+ rte = planner_rt_fetch(foreignrel->relid, root);
+
+ /* For now only this RTE type can come without fpinfo */
+ Assert(rte && rte->rtekind == RTE_FUNCTION);
+ Assert(list_length(rte->functions) == 1);
+
+ rtfunc = (RangeTblFunction *) linitial(rte->functions);
+
+ return is_foreign_expr(root, foreignrel, (Expr *) rtfunc->funcexpr);
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5430,13 +5638,37 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
jointype != JOIN_RIGHT && jointype != JOIN_FULL)
return false;
+ fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+
+ if (!fpinfo_o && !fpinfo_i)
+ return false;
+
+ if (!fpinfo_o)
+ {
+
+ initialize_nonrel_fpinfo(root, outerrel, fpinfo_i);
+
+ if (!is_nonrel_relinfo_ok(root, outerrel))
+ return false;
+
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ }
+ if (!fpinfo_i)
+ {
+ initialize_nonrel_fpinfo(root, innerrel, fpinfo_o);
+
+ if (!is_nonrel_relinfo_ok(root, innerrel))
+ return false;
+
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ }
+
/*
* If either of the joining relations is marked as unsafe to pushdown, the
* join can not be pushed down.
*/
- fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
!fpinfo_i || !fpinfo_i->pushdown_safe)
return false;
@@ -5578,6 +5810,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
case JOIN_RIGHT:
fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
fpinfo_o->remote_conds);
+
fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
fpinfo_i->remote_conds);
break;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 78379bdea5b..da25c535dd5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -3262,3 +3262,63 @@ DROP TABLE join_tbl;
ALTER SERVER loopback OPTIONS (DROP async_capable);
ALTER SERVER loopback2 OPTIONS (DROP async_capable);
+
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = r1.c AND r1.c = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+
+DROP FUNCTION f(INTEGER);
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index e105a4d5f1d..80f29482ef8 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -22,12 +22,14 @@
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
+#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/placeholder.h"
#include "optimizer/plancat.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/tlist.h"
+#include "parser/parsetree.h"
#include "utils/hsearch.h"
#include "utils/lsyscache.h"
@@ -53,7 +55,8 @@ static List *subbuild_joinrel_restrictlist(RelOptInfo *joinrel,
static List *subbuild_joinrel_joinlist(RelOptInfo *joinrel,
List *joininfo_list,
List *new_joininfo);
-static void set_foreign_rel_properties(RelOptInfo *joinrel,
+static void set_foreign_rel_properties(PlannerInfo *root,
+ RelOptInfo *joinrel,
RelOptInfo *outer_rel, RelOptInfo *inner_rel);
static void add_join_rel(PlannerInfo *root, RelOptInfo *joinrel);
static void build_joinrel_partition_info(RelOptInfo *joinrel,
@@ -498,7 +501,7 @@ find_join_rel(PlannerInfo *root, Relids relids)
*
*/
static void
-set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
+set_foreign_rel_properties(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outer_rel,
RelOptInfo *inner_rel)
{
if (OidIsValid(outer_rel->serverid) &&
@@ -528,6 +531,53 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if ((OidIsValid(outer_rel->serverid) &&
+ inner_rel->serverid == InvalidOid) ||
+ (OidIsValid(inner_rel->serverid) &&
+ outer_rel->serverid == InvalidOid))
+ {
+ RelOptInfo *foreign_rel;
+ RelOptInfo *local_rel;
+ RangeTblEntry *rte;
+
+ foreign_rel = OidIsValid(outer_rel->serverid) ? outer_rel : inner_rel;
+ local_rel = OidIsValid(outer_rel->serverid) ? inner_rel : outer_rel;
+ rte = planner_rt_fetch(local_rel->relid, root);
+
+ if (!rte)
+ return;
+
+ switch (rte->rtekind)
+ {
+ case RTE_FUNCTION:
+ {
+ ListCell *lc;
+
+ /* For now support only one function */
+ if (list_length(rte->functions) > 1)
+ return;
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (contain_var_clause(rtfunc->funcexpr) ||
+ contain_mutable_functions(rtfunc->funcexpr) ||
+ contain_subplans(rtfunc->funcexpr))
+ return;
+ }
+ break;
+ }
+ default:
+ /* Avoid pushing unsupported RelOptInfo */
+ return;
+ }
+
+ joinrel->serverid = foreign_rel->serverid;
+ joinrel->userid = foreign_rel->userid;
+ joinrel->useridiscurrent = foreign_rel->useridiscurrent;
+ joinrel->fdwroutine = foreign_rel->fdwroutine;
+ }
}
/*
@@ -674,7 +724,7 @@ build_join_rel(PlannerInfo *root,
joinrel->nullable_partexprs = NULL;
/* Compute information relevant to the foreign relations. */
- set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
+ set_foreign_rel_properties(root, joinrel, outer_rel, inner_rel);
/*
* Create a new tlist containing just the vars that need to be output from
@@ -855,7 +905,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
inner_rel->top_parent_relids);
/* Compute information relevant to foreign relations. */
- set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
+ set_foreign_rel_properties(root, joinrel, outer_rel, inner_rel);
/* Compute information needed for mapping Vars to the child rel */
appinfos = find_appinfos_by_relids(root, joinrel->relids, &nappinfos);
--
2.25.1
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2021-05-20 17:43 Function scan FDW pushdown Alexander Pyhalov <[email protected]>
@ 2021-06-15 13:15 ` Ashutosh Bapat <[email protected]>
2021-10-04 07:42 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Ashutosh Bapat @ 2021-06-15 13:15 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; Ashutosh Bapat <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
Hi Alexander,
On Thu, May 20, 2021 at 11:13 PM Alexander Pyhalov
<[email protected]> wrote:
>
> Hi.
>
> The attached patch allows pushing joins with function RTEs to PostgreSQL
> data sources.
> This makes executing queries like this
>
> create foreign table f_pgbench_accounts (aid int, bid int, abalance int,
> filler char(84)) SERVER local_srv OPTIONS (table_name
> 'pgbench_accounts');
> select * from f_pgbench_accounts join unnest(array[1,2,3]) ON unnest =
> aid;
>
It will be good to provide some practical examples where this is useful.
> more efficient.
>
> with patch:
>
>
> So far I don't know how to visualize actual function expression used in
> function RTE, as in postgresExplainForeignScan() es->rtable comes from
> queryDesc->plannedstmt->rtable, and rte->functions is already 0.
The actual function expression will be part of the Remote SQL of
ForeignScan node so no need to visualize it separately.
The patch will have problems when there are multiple foreign tables
all on different servers or use different FDWs. In such a case the
function scan's RelOptInfo will get the fpinfo based on the first
foreign table the function scan is paired with during join planning.
But that may not be the best foreign table to join. We should be able
to plan all the possible joins. Current infra to add one fpinfo per
RelOptInfo won't help there. We need something better.
The patch targets only postgres FDW, how do you see this working with
other FDWs?
If we come up with the right approach we could use it for 1. pushing
down queries with IN () clause 2. joining a small local table with a
large foreign table by sending the local table rows down to the
foreign server.
--
Best Wishes,
Ashutosh Bapat
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2021-05-20 17:43 Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2021-06-15 13:15 ` Re: Function scan FDW pushdown Ashutosh Bapat <[email protected]>
@ 2021-10-04 07:42 ` Alexander Pyhalov <[email protected]>
2024-11-05 16:11 ` Re: Function scan FDW pushdown [email protected]
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Pyhalov @ 2021-10-04 07:42 UTC (permalink / raw)
To: Ashutosh Bapat <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Ashutosh Bapat писал 2021-06-15 16:15:
> Hi Alexander,
Hi.
The current version of the patch is based on asymetric partition-wise
join.
Currently it is applied after
v19-0001-Asymmetric-partitionwise-join.patch from
on
https://www.postgresql.org/message-id/[email protected]
.
>> So far I don't know how to visualize actual function expression used
>> in
>> function RTE, as in postgresExplainForeignScan() es->rtable comes from
>> queryDesc->plannedstmt->rtable, and rte->functions is already 0.
>
> The actual function expression will be part of the Remote SQL of
> ForeignScan node so no need to visualize it separately.
We still need to create tuple description for functions in
get_tupdesc_for_join_scan_tuples(),
so I had to remove setting newrte->functions to NIL in
add_rte_to_flat_rtable().
With rte->functions in place, there's no issues for explain.
>
> The patch will have problems when there are multiple foreign tables
> all on different servers or use different FDWs. In such a case the
> function scan's RelOptInfo will get the fpinfo based on the first
> foreign table the function scan is paired with during join planning.
> But that may not be the best foreign table to join. We should be able
> to plan all the possible joins. Current infra to add one fpinfo per
> RelOptInfo won't help there. We need something better.
I suppose attached version of the patch is more mature.
>
> The patch targets only postgres FDW, how do you see this working with
> other FDWs?
Not now. We introduce necessary APIs for other FDWs, but implementing
TryShippableJoinPaths()
doesn't seem straightforward.
>
> If we come up with the right approach we could use it for 1. pushing
> down queries with IN () clause 2. joining a small local table with a
> large foreign table by sending the local table rows down to the
> foreign server.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] 1-0001-Push-join-with-function-scan-to-remote-server.patch (103.6K, ../../[email protected]/2-1-0001-Push-join-with-function-scan-to-remote-server.patch)
download | inline diff:
From d997c313daf0031b812d3fca59d338be1a4f2196 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 17 May 2021 19:19:31 +0300
Subject: [PATCH] Push join with function scan to remote server
---
contrib/postgres_fdw/deparse.c | 199 ++-
.../postgres_fdw/expected/postgres_fdw.out | 1095 +++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 497 +++++++-
contrib/postgres_fdw/postgres_fdw.h | 6 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 336 +++++
src/backend/optimizer/path/joinpath.c | 11 +
src/backend/optimizer/plan/setrefs.c | 1 -
src/backend/optimizer/util/relnode.c | 2 +
src/include/foreign/fdwapi.h | 1 +
9 files changed, 2035 insertions(+), 113 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index d98bd666818..7f08575ef60 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -151,6 +151,7 @@ static void deparseConst(Const *node, deparse_expr_cxt *context, int showtype);
static void deparseParam(Param *node, deparse_expr_cxt *context);
static void deparseSubscriptingRef(SubscriptingRef *node, deparse_expr_cxt *context);
static void deparseFuncExpr(FuncExpr *node, deparse_expr_cxt *context);
+static void deparseFuncColnames(StringInfo buf, int varno, RangeTblEntry *rte, bool qualify_col);
static void deparseOpExpr(OpExpr *node, deparse_expr_cxt *context);
static void deparseOperatorName(StringInfo buf, Form_pg_operator opform);
static void deparseDistinctExpr(DistinctExpr *node, deparse_expr_cxt *context);
@@ -1740,13 +1741,54 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
{
RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
- /*
- * Core code already has some lock on each rel being planned, so we
- * can use NoLock here.
- */
- Relation rel = table_open(rte->relid, NoLock);
+ Assert(rte->rtekind == RTE_RELATION || rte->rtekind == RTE_FUNCTION);
+ if (rte->rtekind == RTE_RELATION)
+ {
+ /*
+ * Core code already has some lock on each rel being planned, so
+ * we can use NoLock here.
+ */
+ Relation rel = table_open(rte->relid, NoLock);
- deparseRelation(buf, rel);
+ deparseRelation(buf, rel);
+
+ table_close(rel, NoLock);
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ RangeTblFunction *rtfunc;
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool first = true;
+ int n;
+
+ n = list_length(rte->functions);
+ Assert(n >= 1);
+
+ if (n > 1)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ if (!first)
+ appendStringInfoString(buf, ", ");
+ else
+ first = false;
+
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ context.root = root;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.buf = buf;
+ context.params_list = params_list;
+
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ }
+
+ if (n > 1)
+ appendStringInfoString(buf, ")");
+ }
/*
* Add a unique alias to avoid any conflict in relation names due to
@@ -1754,9 +1796,43 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
* join.
*/
if (use_alias)
+ {
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ appendStringInfo(buf, " (");
+ deparseFuncColnames(buf, 0, rte, false);
+ appendStringInfo(buf, ") ");
+ }
+ }
+ }
+}
- table_close(rel, NoLock);
+/*
+ * Deparse function columns alias list
+ */
+static void
+deparseFuncColnames(StringInfo buf, int varno, RangeTblEntry *rte, bool qualify_col)
+{
+ bool first = true;
+ ListCell *lc;
+
+ Assert(rte);
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(rte->eref);
+
+ foreach(lc, rte->eref->colnames)
+ {
+ char *colname = strVal(lfirst(lc));
+
+ if (colname[0] == '\0')
+ continue;
+ if (!first)
+ appendStringInfoString(buf, ",");
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
+ appendStringInfoString(buf, quote_identifier(colname));
+ first = false;
}
}
@@ -2057,7 +2133,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
appendStringInfoString(buf, "UPDATE ");
deparseRelation(buf, rel);
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
appendStringInfoString(buf, " SET ");
@@ -2084,7 +2160,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
reset_transmission_modes(nestlevel);
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
{
List *ignore_conds = NIL;
@@ -2100,7 +2176,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
appendConditions(remote_conds, &context);
}
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
deparseExplicitTargetList(returningList, true, retrieved_attrs,
&context);
else
@@ -2164,10 +2240,10 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
appendStringInfoString(buf, "DELETE FROM ");
deparseRelation(buf, rel);
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
{
List *ignore_conds = NIL;
@@ -2183,7 +2259,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
appendConditions(remote_conds, &context);
}
- if (foreignrel->reloptkind == RELOPT_JOINREL)
+ if (IS_JOIN_REL(foreignrel))
deparseExplicitTargetList(returningList, true, retrieved_attrs,
&context);
else
@@ -2407,23 +2483,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
/* Required only to be passed down to deparseTargetList(). */
List *retrieved_attrs;
- /*
- * The lock on the relation will be held by upper callers, so it's
- * fine to open it with no lock here.
- */
- rel = table_open(rte->relid, NoLock);
-
- /*
- * The local name of the foreign table can not be recognized by the
- * foreign server and the table it references on foreign server might
- * have different column ordering or different columns than those
- * declared locally. Hence we have to deparse whole-row reference as
- * ROW(columns referenced locally). Construct this by deparsing a
- * "whole row" attribute.
- */
- attrs_used = bms_add_member(NULL,
- 0 - FirstLowInvalidHeapAttributeNumber);
-
/*
* In case the whole-row reference is under an outer join then it has
* to go NULL whenever the rest of the row goes NULL. Deparsing a join
@@ -2438,16 +2497,43 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
}
appendStringInfoString(buf, "ROW(");
- deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
- &retrieved_attrs);
+ if (rte->rtekind == RTE_RELATION)
+ {
+ /*
+ * The local name of the foreign table can not be recognized by
+ * the foreign server and the table it references on foreign
+ * server might have different column ordering or different
+ * columns than those declared locally. Hence we have to deparse
+ * whole-row reference as ROW(columns referenced locally).
+ * Construct this by deparsing a "whole row" attribute.
+ */
+ attrs_used = bms_add_member(NULL,
+ 0 - FirstLowInvalidHeapAttributeNumber);
+
+ /*
+ * The lock on the relation will be held by upper callers, so it's
+ * fine to open it with no lock here.
+ */
+ rel = table_open(rte->relid, NoLock);
+ deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
+ &retrieved_attrs);
+ table_close(rel, NoLock);
+ bms_free(attrs_used);
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * Function call is translated as-is, function returns the same
+ * columns in the same order as on local server
+ */
+ deparseFuncColnames(buf, varno, rte, qualify_col);
+ }
appendStringInfoChar(buf, ')');
/* Complete the CASE WHEN statement started above. */
if (qualify_col)
appendStringInfoString(buf, " END");
- table_close(rel, NoLock);
- bms_free(attrs_used);
}
else
{
@@ -2462,29 +2548,40 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
* If it's a column of a foreign table, and it has the column_name FDW
* option, use that value.
*/
- options = GetForeignColumnOptions(rte->relid, varattno);
- foreach(lc, options)
+ if (rte->rtekind == RTE_RELATION)
{
- DefElem *def = (DefElem *) lfirst(lc);
-
- if (strcmp(def->defname, "column_name") == 0)
+ options = GetForeignColumnOptions(rte->relid, varattno);
+ foreach(lc, options)
{
- colname = defGetString(def);
- break;
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "column_name") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
}
- }
- /*
- * If it's a column of a regular table or it doesn't have column_name
- * FDW option, use attribute name.
- */
- if (colname == NULL)
- colname = get_attname(rte->relid, varattno, false);
+ /*
+ * If it's a column of a regular table or it doesn't have
+ * column_name FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, varattno, false);
- if (qualify_col)
- ADD_REL_QUALIFIER(buf, varno);
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
- appendStringInfoString(buf, quote_identifier(colname));
+ appendStringInfoString(buf, quote_identifier(colname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ colname = get_rte_attribute_name(rte, varattno);
+
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
+ appendStringInfoString(buf, quote_identifier(colname));
+ }
}
}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index c7b7db80650..bc896914fd1 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -10761,3 +10761,1098 @@ ERROR: invalid value for integer option "fetch_size": 100$%$#$#
CREATE FOREIGN TABLE inv_bsz (c1 int )
SERVER loopback OPTIONS (batch_size '100$%$#$#');
ERROR: invalid value for integer option "batch_size": 100$%$#$#
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r2.n FROM (public.base_tbl r1 INNER JOIN unnest('{2,3,4}'::integer[]) r2 (n) ON (((r1.a = r2.n))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+ a | b | n
+---+---+---
+ 2 | 4 | 2
+ 3 | 6 | 3
+ 4 | 8 | 4
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r2.a, r2.b FROM (public.base_tbl r2 INNER JOIN unnest('{2,3,4}'::integer[]) r1 (n) ON (((r1.n = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+ n | a | b
+---+---+---
+ 2 | 2 | 4
+ 3 | 3 | 6
+ 4 | 4 | 8
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, r1.c, r1.d, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r2.c, r2.d, r3.n FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r1.a = r3.n))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a
+ORDER BY r.a;
+ a | b | c | d | n
+---+---+---+-------+---
+ 3 | 6 | 3 | text3 | 3
+ 4 | 8 | 4 | text4 | 4
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r3.n FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r1.a = r3.n)) AND ((r3.n > 3))))
+(4 rows)
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*, t.n from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), json_each_text('{"a":"text1", "c":"text4"}')) t (n, k, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.c, r.d, t.n
+ Relations: (public.remote_tbl1 r) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.json_each_text()) t)
+ Remote SQL: SELECT r1.c, r1.d, r2.n FROM (public.base_tbl1 r1 INNER JOIN ROWS FROM (unnest('{3,4}'::integer[]), json_each_text('{"a":"text1", "c":"text4"}'::json)) r2 (n,k,txt) ON (((r1.c = r2.n)) AND ((r1.d = r2.txt))))
+(4 rows)
+
+SELECT r.*, t.txt from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), json_each_text('{"a":"text1", "c":"text4"}')) t (n, k, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+ c | d | txt
+---+-------+-------
+ 4 | text4 | text4
+(1 row)
+
+-- complex types
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx);
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, t.bx
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() t)
+ Remote SQL: SELECT r1.a, r1.b, r2.bx FROM (public.base_tbl r1 INNER JOIN unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) ON (((r1.a = area(r2.bx)))))
+(4 rows)
+
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx)
+ORDER BY r.a;
+ a | b | bx
+----+----+---------------
+ 24 | 48 | (2,3),(-2,-3)
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') ON d = value;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r1.c, r1.d, json_each_text.key, json_each_text.value
+ Relations: (public.remote_tbl1 r1) INNER JOIN (pg_catalog.json_each_text())
+ Remote SQL: SELECT r1.c, r1.d, r2.key, r2.value FROM (public.base_tbl1 r1 INNER JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}'::json) r2 (key,value) ON (((r1.d = r2.value))))
+(4 rows)
+
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') ON d = value
+ORDER BY r1.c;
+ c | d | key | value
+----+--------+-----+--------
+ 1 | text1 | a | text1
+ 14 | text14 | c | text14
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') AS t(u,v) ON d = v;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r1.c, r1.d, t.u, t.v
+ Relations: (public.remote_tbl1 r1) INNER JOIN (pg_catalog.json_each_text() t)
+ Remote SQL: SELECT r1.c, r1.d, r2.u, r2.v FROM (public.base_tbl1 r1 INNER JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}'::json) r2 (u,v) ON (((r1.d = r2.v))))
+(4 rows)
+
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') AS t(u,v) ON d = v
+ORDER BY r1.c;
+ c | d | u | v
+----+--------+---+--------
+ 1 | text1 | a | text1
+ 14 | text14 | c | text14
+(2 rows)
+
+-- DML
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.base_tbl r1 SET b = 5 FROM unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) WHERE ((r1.a = area(r2.bx))) RETURNING r1.a, r1.b
+(4 rows)
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
+ -> Foreign Scan
+ Output: CASE WHEN (random() >= '0'::double precision) THEN 5 ELSE 0 END, r.ctid, r.*, t.*
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() t)
+ Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.a, r1.b) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.bx) END FROM (public.base_tbl r1 INNER JOIN unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) ON (((r1.a = area(r2.bx))))) FOR UPDATE OF r1
+ -> Hash Join
+ Output: r.ctid, r.*, t.*
+ Hash Cond: ((r.a)::double precision = area(t.bx))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.ctid, r.*, r.a
+ Remote SQL: SELECT a, b, ctid FROM public.base_tbl FOR UPDATE
+ -> Hash
+ Output: t.*, t.bx
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.*, t.bx
+ Function Call: unnest('{(2,3),(-2,-3)}'::box[])
+(18 rows)
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.base_tbl r1 SET b = 5 FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r1.a >= r2.l)) AND ((r1.a <= area(r2.bx))) RETURNING r1.a, r1.b
+(4 rows)
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 10 | 5
+ 11 | 5
+ 12 | 5
+ 13 | 5
+ 14 | 5
+ 15 | 5
+ 16 | 5
+ 17 | 5
+ 18 | 5
+ 19 | 5
+ 20 | 5
+ 21 | 5
+ 22 | 5
+ 23 | 5
+ 25 | 5
+ 26 | 5
+ 27 | 5
+ 28 | 5
+ 24 | 5
+(19 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
+ -> Foreign Scan
+ Output: CASE WHEN (random() >= '0'::double precision) THEN 5 ELSE 0 END, r.ctid, r.*, t.*
+ Relations: (public.remote_tbl r) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.unnest()) t)
+ Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.a, r1.b) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.l,r2.bx) END FROM (public.base_tbl r1 INNER JOIN ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) ON (((r1.a >= r2.l)) AND ((r1.a <= area(r2.bx))))) FOR UPDATE OF r1
+ -> Nested Loop
+ Output: r.ctid, r.*, t.*
+ Join Filter: ((r.a >= t.l) AND ((r.a)::double precision <= area(t.bx)))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.ctid, r.*, r.a
+ Remote SQL: SELECT a, b, ctid FROM public.base_tbl FOR UPDATE
+ -> Function Scan on t
+ Output: t.*, t.l, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+(16 rows)
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 10 | 5
+ 11 | 5
+ 12 | 5
+ 13 | 5
+ 14 | 5
+ 15 | 5
+ 16 | 5
+ 17 | 5
+ 18 | 5
+ 19 | 5
+ 20 | 5
+ 21 | 5
+ 22 | 5
+ 23 | 5
+ 25 | 5
+ 26 | 5
+ 27 | 5
+ 28 | 5
+ 24 | 5
+(19 rows)
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: r.a, r.b, (generate_series(1, 10))
+ Hash Cond: (r.a = (generate_series(1, 10)))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl
+ -> Hash
+ Output: (generate_series(1, 10))
+ -> ProjectSet
+ Output: generate_series(1, 10)
+ -> Result
+(11 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+-- Test joins with append relations
+SET enable_partitionwise_join=on;
+-- Partitioned tables and function scan pushdown
+CREATE TABLE distr1(a int, b int) PARTITION BY HASH(a);
+CREATE TABLE distr1_part_1 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 0);
+CREATE TABLE distr1_base_2 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_2 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr1_base_2', use_remote_estimate 'true');
+CREATE TABLE distr1_base_3 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_3 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 2)
+ SERVER loopback2 OPTIONS (table_name 'distr1_base_3', use_remote_estimate 'true');
+CREATE TABLE distr1_base_4 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_4 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 3)
+ SERVER loopback OPTIONS (table_name 'distr1_base_4', use_remote_estimate 'true');
+CREATE TABLE distr2(c int, d text) PARTITION BY HASH(c);
+CREATE TABLE distr2_part_1 PARTITION OF distr2 FOR VALUES WITH (MODULUS 4, REMAINDER 0);
+CREATE TABLE distr2_base_2 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_2 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr2_base_2', use_remote_estimate 'true');
+CREATE TABLE distr2_base_3 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_3 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 2)
+ SERVER loopback2 OPTIONS (table_name 'distr2_base_3', use_remote_estimate 'true');
+CREATE TABLE distr2_base_4 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_4 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 3)
+ SERVER loopback OPTIONS (table_name 'distr2_base_4', use_remote_estimate 'true');
+INSERT INTO distr1 SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO distr2 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE distr1;
+ANALYZE distr1_part_1;
+ANALYZE distr1_base_2;
+ANALYZE distr1_base_3;
+ANALYZE distr1_base_4;
+ANALYZE distr2;
+ANALYZE distr2_base_2;
+ANALYZE distr2_base_3;
+ANALYZE distr2_base_4;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: d_1.a, d_1.b, n.n
+ Hash Cond: (d_1.a = n.n)
+ -> Seq Scan on public.distr1_part_1 d_1
+ Output: d_1.a, d_1.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Foreign Scan
+ Output: d_2.a, d_2.b, n.n
+ Relations: (public.distr1_part_2 d_2) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r4.a, r4.b, r2.n FROM (public.distr1_base_2 r4 INNER JOIN unnest('{2,3,4,5}'::integer[]) r2 (n) ON (((r4.a = r2.n))))
+ -> Foreign Scan
+ Output: d_3.a, d_3.b, n.n
+ Relations: (public.distr1_part_3 d_3) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r5.a, r5.b, r2.n FROM (public.distr1_base_3 r5 INNER JOIN unnest('{2,3,4,5}'::integer[]) r2 (n) ON (((r5.a = r2.n))))
+ -> Foreign Scan
+ Output: d_4.a, d_4.b, n.n
+ Relations: (public.distr1_part_4 d_4) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r6.a, r6.b, r2.n FROM (public.distr1_base_4 r6 INNER JOIN unnest('{2,3,4,5}'::integer[]) r2 (n) ON (((r6.a = r2.n))))
+(23 rows)
+
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n
+ORDER BY d.a;
+ a | b | n
+---+----+---
+ 2 | 4 | 2
+ 3 | 6 | 3
+ 4 | 8 | 4
+ 5 | 10 | 5
+(4 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n;
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: n.n, d_1.a, d_1.b
+ Hash Cond: (d_1.a = n.n)
+ -> Seq Scan on public.distr1_part_1 d_1
+ Output: d_1.a, d_1.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Foreign Scan
+ Output: n.n, d_2.a, d_2.b
+ Relations: (public.distr1_part_2 d_2) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r4.a, r4.b FROM (public.distr1_base_2 r4 INNER JOIN unnest('{2,3,4,5}'::integer[]) r1 (n) ON (((r1.n = r4.a))))
+ -> Foreign Scan
+ Output: n.n, d_3.a, d_3.b
+ Relations: (public.distr1_part_3 d_3) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r5.a, r5.b FROM (public.distr1_base_3 r5 INNER JOIN unnest('{2,3,4,5}'::integer[]) r1 (n) ON (((r1.n = r5.a))))
+ -> Foreign Scan
+ Output: n.n, d_4.a, d_4.b
+ Relations: (public.distr1_part_4 d_4) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r6.a, r6.b FROM (public.distr1_base_4 r6 INNER JOIN unnest('{2,3,4,5}'::integer[]) r1 (n) ON (((r1.n = r6.a))))
+(23 rows)
+
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n
+ORDER BY d.a;
+ n | a | b
+---+---+----
+ 2 | 2 | 4
+ 3 | 3 | 6
+ 4 | 4 | 8
+ 5 | 5 | 10
+(4 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.c, d2_1.d, n.n
+ Hash Cond: (d1_1.a = n.n)
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.c, d2_1.d
+ Hash Cond: (d1_1.a = d2_1.c)
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.b
+ -> Hash
+ Output: d2_1.c, d2_1.d
+ -> Seq Scan on public.distr2_part_1 d2_1
+ Output: d2_1.c, d2_1.d
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{3,4}'::integer[])
+ -> Foreign Scan
+ Output: d1_2.a, d1_2.b, d2_2.c, d2_2.d, n.n
+ Relations: ((public.distr1_part_2 d1_2) INNER JOIN (public.distr2_part_2 d2_2)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r5.a, r5.b, r9.c, r9.d, r3.n FROM ((public.distr1_base_2 r5 INNER JOIN public.distr2_base_2 r9 ON (((r5.a = r9.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r5.a = r3.n))))
+ -> Foreign Scan
+ Output: d1_3.a, d1_3.b, d2_3.c, d2_3.d, n.n
+ Relations: ((public.distr1_part_3 d1_3) INNER JOIN (public.distr2_part_3 d2_3)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r6.a, r6.b, r10.c, r10.d, r3.n FROM ((public.distr1_base_3 r6 INNER JOIN public.distr2_base_3 r10 ON (((r6.a = r10.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r6.a = r3.n))))
+ -> Foreign Scan
+ Output: d1_4.a, d1_4.b, d2_4.c, d2_4.d, n.n
+ Relations: ((public.distr1_part_4 d1_4) INNER JOIN (public.distr2_part_4 d2_4)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r7.a, r7.b, r11.c, r11.d, r3.n FROM ((public.distr1_base_4 r7 INNER JOIN public.distr2_base_4 r11 ON (((r7.a = r11.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r7.a = r3.n))))
+(30 rows)
+
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a
+ORDER BY d1.a;
+ a | b | c | d | n
+---+---+---+-------+---
+ 3 | 6 | 3 | text3 | 3
+ 4 | 8 | 4 | text4 | 4
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.c, d2_1.d, n.n
+ Hash Cond: (d1_1.a = n.n)
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.c, d2_1.d
+ Hash Cond: (d1_1.a = d2_1.c)
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.b
+ -> Hash
+ Output: d2_1.c, d2_1.d
+ -> Seq Scan on public.distr2_part_1 d2_1
+ Output: d2_1.c, d2_1.d
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{3,4}'::integer[])
+ Filter: (n.n > 3)
+ -> Foreign Scan
+ Output: d1_2.a, d1_2.b, d2_2.c, d2_2.d, n.n
+ Relations: ((public.distr1_part_2 d1_2) INNER JOIN (public.distr2_part_2 d2_2)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r5.a, r5.b, r9.c, r9.d, r3.n FROM ((public.distr1_base_2 r5 INNER JOIN public.distr2_base_2 r9 ON (((r5.a = r9.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r5.a = r3.n)) AND ((r3.n > 3))))
+ -> Foreign Scan
+ Output: d1_3.a, d1_3.b, d2_3.c, d2_3.d, n.n
+ Relations: ((public.distr1_part_3 d1_3) INNER JOIN (public.distr2_part_3 d2_3)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r6.a, r6.b, r10.c, r10.d, r3.n FROM ((public.distr1_base_3 r6 INNER JOIN public.distr2_base_3 r10 ON (((r6.a = r10.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r6.a = r3.n)) AND ((r3.n > 3))))
+ -> Foreign Scan
+ Output: d1_4.a, d1_4.b, d2_4.c, d2_4.d, n.n
+ Relations: ((public.distr1_part_4 d1_4) INNER JOIN (public.distr2_part_4 d2_4)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r7.a, r7.b, r11.c, r11.d, r3.n FROM ((public.distr1_base_4 r7 INNER JOIN public.distr2_base_4 r11 ON (((r7.a = r11.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r7.a = r3.n)) AND ((r3.n > 3))))
+(31 rows)
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3
+ORDER BY d1.a;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+-- Direct update with returning
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.distr1 d1
+ Output: d1_1.a, d1_1.b
+ Update on public.distr1_part_1 d1_1
+ Foreign Update on public.distr1_part_2 d1_2
+ Foreign Update on public.distr1_part_3 d1_3
+ Foreign Update on public.distr1_part_4 d1_4
+ -> Result
+ Output: t.l, t.*, d1.tableoid, d1.ctid, (NULL::record)
+ -> Append
+ -> Nested Loop
+ Output: d1_1.tableoid, d1_1.ctid, NULL::record, t.l, t.*
+ Join Filter: ((d1_1.a >= t.l) AND ((d1_1.a)::double precision <= area(t.bx)))
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+ -> Materialize
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_2 r4 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r4.a >= r2.l)) AND ((r4.a <= area(r2.bx))) RETURNING CASE WHEN (r4.*)::text IS NOT NULL THEN 17254 END, r4.a, r4.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_3 r5 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r5.a >= r2.l)) AND ((r5.a <= area(r2.bx))) RETURNING CASE WHEN (r5.*)::text IS NOT NULL THEN 17260 END, r5.a, r5.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_4 r6 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r6.a >= r2.l)) AND ((r6.a <= area(r2.bx))) RETURNING CASE WHEN (r6.*)::text IS NOT NULL THEN 17266 END, r6.a, r6.b
+(25 rows)
+
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+----
+ 12 | 10
+ 14 | 10
+ 16 | 10
+ 17 | 10
+ 26 | 10
+ 28 | 10
+ 11 | 10
+ 19 | 10
+ 20 | 10
+ 21 | 10
+ 13 | 10
+ 18 | 10
+ 23 | 10
+ 25 | 10
+ 27 | 10
+ 10 | 10
+ 15 | 10
+ 22 | 10
+ 24 | 10
+(19 rows)
+
+-- Direct update with returning tableoid
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b,tableoid;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.distr1 d1
+ Output: d1_1.a, d1_1.b, d1_1.tableoid
+ Update on public.distr1_part_1 d1_1
+ Foreign Update on public.distr1_part_2 d1_2
+ Foreign Update on public.distr1_part_3 d1_3
+ Foreign Update on public.distr1_part_4 d1_4
+ -> Result
+ Output: t.l, t.*, d1.tableoid, d1.ctid, (NULL::record)
+ -> Append
+ -> Nested Loop
+ Output: d1_1.tableoid, d1_1.ctid, NULL::record, t.l, t.*
+ Join Filter: ((d1_1.a >= t.l) AND ((d1_1.a)::double precision <= area(t.bx)))
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+ -> Materialize
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_2 r4 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r4.a >= r2.l)) AND ((r4.a <= area(r2.bx))) RETURNING CASE WHEN (r4.*)::text IS NOT NULL THEN 17254 END, r4.a, r4.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_3 r5 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r5.a >= r2.l)) AND ((r5.a <= area(r2.bx))) RETURNING CASE WHEN (r5.*)::text IS NOT NULL THEN 17260 END, r5.a, r5.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.distr1_base_4 r6 SET b = r2.l FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r6.a >= r2.l)) AND ((r6.a <= area(r2.bx))) RETURNING CASE WHEN (r6.*)::text IS NOT NULL THEN 17266 END, r6.a, r6.b
+(25 rows)
+
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b,tableoid;
+ a | b | tableoid
+----+----+----------
+ 12 | 10 | 17248
+ 14 | 10 | 17248
+ 16 | 10 | 17248
+ 17 | 10 | 17248
+ 26 | 10 | 17248
+ 28 | 10 | 17248
+ 11 | 10 | 17254
+ 19 | 10 | 17254
+ 20 | 10 | 17254
+ 21 | 10 | 17254
+ 13 | 10 | 17260
+ 18 | 10 | 17260
+ 23 | 10 | 17260
+ 25 | 10 | 17260
+ 27 | 10 | 17260
+ 10 | 10 | 17266
+ 15 | 10 | 17266
+ 22 | 10 | 17266
+ 24 | 10 | 17266
+(19 rows)
+
+-- Indirect update with returning
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=CASE WHEN random()>=0 THEN t.l ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.distr1 d1
+ Output: d1_1.a, d1_1.b
+ Update on public.distr1_part_1 d1_1
+ Foreign Update on public.distr1_part_2 d1_2
+ Remote SQL: UPDATE public.distr1_base_2 SET b = $2 WHERE ctid = $1 RETURNING a, b
+ Foreign Update on public.distr1_part_3 d1_3
+ Remote SQL: UPDATE public.distr1_base_3 SET b = $2 WHERE ctid = $1 RETURNING a, b
+ Foreign Update on public.distr1_part_4 d1_4
+ Remote SQL: UPDATE public.distr1_base_4 SET b = $2 WHERE ctid = $1 RETURNING a, b
+ -> Result
+ Output: CASE WHEN (random() >= '0'::double precision) THEN t.l ELSE 0 END, t.*, d1.tableoid, d1.ctid, (NULL::record)
+ -> Append
+ -> Nested Loop
+ Output: d1_1.tableoid, d1_1.ctid, NULL::record, t.l, t.*
+ Join Filter: ((d1_1.a >= t.l) AND ((d1_1.a)::double precision <= area(t.bx)))
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+ -> Materialize
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.tableoid, d1_1.ctid, NULL::record
+ -> Foreign Scan
+ Output: d1_2.tableoid, d1_2.ctid, d1_2.*, t.l, t.*
+ Relations: (public.distr1_part_2 d1_2) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.unnest()) t)
+ Remote SQL: SELECT CASE WHEN (r4.*)::text IS NOT NULL THEN 17254 END, r4.ctid, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.a, r4.b) END, r2.l, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.l,r2.bx) END FROM (public.distr1_base_2 r4 INNER JOIN ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) ON (((r4.a >= r2.l)) AND ((r4.a <= area(r2.bx))))) FOR UPDATE OF r4
+ -> Nested Loop
+ Output: d1_2.tableoid, d1_2.ctid, d1_2.*, t.l, t.*
+ Join Filter: ((d1_2.a >= t.l) AND ((d1_2.a)::double precision <= area(t.bx)))
+ -> Foreign Scan on public.distr1_part_2 d1_2
+ Output: d1_2.a, d1_2.tableoid, d1_2.ctid, d1_2.*
+ Remote SQL: SELECT a, b, ctid FROM public.distr1_base_2 FOR UPDATE
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+ -> Foreign Scan
+ Output: d1_3.tableoid, d1_3.ctid, d1_3.*, t.l, t.*
+ Relations: (public.distr1_part_3 d1_3) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.unnest()) t)
+ Remote SQL: SELECT CASE WHEN (r5.*)::text IS NOT NULL THEN 17260 END, r5.ctid, CASE WHEN (r5.*)::text IS NOT NULL THEN ROW(r5.a, r5.b) END, r2.l, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.l,r2.bx) END FROM (public.distr1_base_3 r5 INNER JOIN ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) ON (((r5.a >= r2.l)) AND ((r5.a <= area(r2.bx))))) FOR UPDATE OF r5
+ -> Nested Loop
+ Output: d1_3.tableoid, d1_3.ctid, d1_3.*, t.l, t.*
+ Join Filter: ((d1_3.a >= t.l) AND ((d1_3.a)::double precision <= area(t.bx)))
+ -> Foreign Scan on public.distr1_part_3 d1_3
+ Output: d1_3.a, d1_3.tableoid, d1_3.ctid, d1_3.*
+ Remote SQL: SELECT a, b, ctid FROM public.distr1_base_3 FOR UPDATE
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+ -> Foreign Scan
+ Output: d1_4.tableoid, d1_4.ctid, d1_4.*, t.l, t.*
+ Relations: (public.distr1_part_4 d1_4) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.unnest()) t)
+ Remote SQL: SELECT CASE WHEN (r6.*)::text IS NOT NULL THEN 17266 END, r6.ctid, CASE WHEN (r6.*)::text IS NOT NULL THEN ROW(r6.a, r6.b) END, r2.l, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.l,r2.bx) END FROM (public.distr1_base_4 r6 INNER JOIN ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) ON (((r6.a >= r2.l)) AND ((r6.a <= area(r2.bx))))) FOR UPDATE OF r6
+ -> Nested Loop
+ Output: d1_4.tableoid, d1_4.ctid, d1_4.*, t.l, t.*
+ Join Filter: ((d1_4.a >= t.l) AND ((d1_4.a)::double precision <= area(t.bx)))
+ -> Foreign Scan on public.distr1_part_4 d1_4
+ Output: d1_4.a, d1_4.tableoid, d1_4.ctid, d1_4.*
+ Remote SQL: SELECT a, b, ctid FROM public.distr1_base_4 FOR UPDATE
+ -> Function Scan on t
+ Output: t.l, t.*, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+(61 rows)
+
+UPDATE distr1 d1 SET b=CASE WHEN random()>=0 THEN t.l ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+----
+ 12 | 10
+ 14 | 10
+ 16 | 10
+ 17 | 10
+ 26 | 10
+ 28 | 10
+ 11 | 10
+ 19 | 10
+ 20 | 10
+ 21 | 10
+ 13 | 10
+ 18 | 10
+ 23 | 10
+ 25 | 10
+ 27 | 10
+ 10 | 10
+ 15 | 10
+ 22 | 10
+ 24 | 10
+(19 rows)
+
+DROP TABLE distr1, distr2, distr1_base_2, distr2_base_2, distr1_base_3, distr2_base_3, distr1_base_4, distr2_base_4;
+-- Test pushdown of several function scans
+CREATE TABLE distr1(a int, b int) PARTITION BY HASH(a);
+CREATE TABLE distr1_part_1 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 2, REMAINDER 0);
+CREATE TABLE distr1_base_2 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_2 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 2, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr1_base_2', use_remote_estimate 'true');
+CREATE TABLE distr2(c int, d text) PARTITION BY HASH(c);
+CREATE TABLE distr2_part_1 PARTITION OF distr2 FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE distr2_base_2 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_2 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 2, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr2_base_2', use_remote_estimate 'true');
+INSERT INTO distr1 SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO distr2 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE;
+-- Make local function scan not so attractive
+ALTER SERVER loopback OPTIONS (ADD fdw_tuple_cost '1000');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr1 d2, unnest(array[3,4]) n, unnest(array[3,4]) g
+WHERE d1.a = n AND d2.a = d1.a and g = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.a, d2_1.b, n.n, g.g
+ Hash Cond: (d1_1.a = g.g)
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.a, d2_1.b, n.n
+ Hash Cond: (d1_1.a = n.n)
+ -> Hash Join
+ Output: d1_1.a, d1_1.b, d2_1.a, d2_1.b
+ Hash Cond: (d1_1.a = d2_1.a)
+ -> Seq Scan on public.distr1_part_1 d1_1
+ Output: d1_1.a, d1_1.b
+ -> Hash
+ Output: d2_1.a, d2_1.b
+ -> Seq Scan on public.distr1_part_1 d2_1
+ Output: d2_1.a, d2_1.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{3,4}'::integer[])
+ -> Hash
+ Output: g.g
+ -> Function Scan on pg_catalog.unnest g
+ Output: g.g
+ Function Call: unnest('{3,4}'::integer[])
+ -> Foreign Scan
+ Output: d1_2.a, d1_2.b, d2_2.a, d2_2.b, n.n, g.g
+ Relations: (((public.distr1_part_2 d1_2) INNER JOIN (public.distr1_part_2 d2_2)) INNER JOIN (pg_catalog.unnest() n)) INNER JOIN (pg_catalog.unnest() g)
+ Remote SQL: SELECT r6.a, r6.b, r8.a, r8.b, r3.n, r4.g FROM (((public.distr1_base_2 r6 INNER JOIN public.distr1_base_2 r8 ON (((r6.a = r8.a)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r6.a = r3.n)))) INNER JOIN unnest('{3,4}'::integer[]) r4 (g) ON (((r6.a = r4.g))))
+(30 rows)
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n, generate_series(1,10000) g
+WHERE d1.a = n AND d2.c = d1.a and g = d1.a
+ORDER BY d1.a;
+ a | b | c | d | n | g
+---+---+---+-------+---+---
+ 3 | 6 | 3 | text3 | 3 | 3
+ 4 | 8 | 4 | text4 | 4 | 4
+(2 rows)
+
+ALTER SERVER loopback OPTIONS (DROP fdw_tuple_cost);
+DROP TABLE distr1, distr2, distr1_base_2, distr2_base_2;
+-- Test inheritance chain and function scan pushdown
+CREATE TABLE distr1(a int, b int);
+CREATE TABLE distr1_loc (a int, b int) INHERITS (distr1);
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+CREATE TABLE distr1_base (a int, b int, c text);
+CREATE FOREIGN TABLE distr1_remote (a int, b int, c text) INHERITS (distr1)
+ SERVER loopback OPTIONS (table_name 'distr1_base', use_remote_estimate 'true');
+NOTICE: merging column "a" with inherited definition
+NOTICE: merging column "b" with inherited definition
+CREATE TABLE distr2(c int, d text);
+CREATE TABLE distr2_loc (c int, d text) INHERITS (distr2);
+NOTICE: merging column "c" with inherited definition
+NOTICE: merging column "d" with inherited definition
+CREATE TABLE distr2_base (c int, d text, e int);
+CREATE FOREIGN TABLE distr2_remote (c int, d text, e int) INHERITS (distr2)
+ SERVER loopback OPTIONS (table_name 'distr2_base', use_remote_estimate 'true');
+NOTICE: merging column "c" with inherited definition
+NOTICE: merging column "d" with inherited definition
+INSERT INTO distr1_loc SELECT g, g*2 from generate_series(1,100) g;
+INSERT INTO distr1_base SELECT g, g*2, 'text'|| g from generate_series(200,20000) g;
+INSERT INTO distr2_loc SELECT g, 'text'|| g from generate_series(1,100) g;
+INSERT INTO distr2_base SELECT g, 'text'|| g, g*2 from generate_series(200,20000) g;
+ANALYZE distr1, distr1_loc, distr1_base;
+ANALYZE distr2, distr2_loc, distr2_base;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: d_1.a, d_1.b, n.n
+ Hash Cond: (n.n = d_1.a)
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Hash
+ Output: d_1.a, d_1.b
+ -> Seq Scan on public.distr1 d_1
+ Output: d_1.a, d_1.b
+ -> Hash Join
+ Output: d_2.a, d_2.b, n.n
+ Hash Cond: (d_2.a = n.n)
+ -> Seq Scan on public.distr1_loc d_2
+ Output: d_2.a, d_2.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Foreign Scan
+ Output: d_3.a, d_3.b, n.n
+ Relations: (public.distr1_remote d_3) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r5.a, r5.b, r2.n FROM (public.distr1_base r5 INNER JOIN unnest('{2,3,4,5}'::integer[]) r2 (n) ON (((r5.a = r2.n))))
+(25 rows)
+
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n
+ORDER BY d.a;
+ a | b | n
+---+----+---
+ 2 | 4 | 2
+ 3 | 6 | 3
+ 4 | 8 | 4
+ 5 | 10 | 5
+(4 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Hash Join
+ Output: n.n, d_1.a, d_1.b
+ Hash Cond: (n.n = d_1.a)
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Hash
+ Output: d_1.a, d_1.b
+ -> Seq Scan on public.distr1 d_1
+ Output: d_1.a, d_1.b
+ -> Hash Join
+ Output: n.n, d_2.a, d_2.b
+ Hash Cond: (d_2.a = n.n)
+ -> Seq Scan on public.distr1_loc d_2
+ Output: d_2.a, d_2.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4,5}'::integer[])
+ -> Foreign Scan
+ Output: n.n, d_3.a, d_3.b
+ Relations: (public.distr1_remote d_3) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r5.a, r5.b FROM (public.distr1_base r5 INNER JOIN unnest('{2,3,4,5}'::integer[]) r1 (n) ON (((r1.n = r5.a))))
+(25 rows)
+
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n
+ORDER BY d.a;
+ n | a | b
+---+---+----
+ 2 | 2 | 4
+ 3 | 3 | 6
+ 4 | 4 | 8
+ 5 | 5 | 10
+(4 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: d1.a, d1.b, d2.c, d2.d, n.n
+ Hash Cond: (d2.c = d1.a)
+ -> Append
+ -> Seq Scan on public.distr2 d2_1
+ Output: d2_1.c, d2_1.d
+ -> Seq Scan on public.distr2_loc d2_2
+ Output: d2_2.c, d2_2.d
+ -> Foreign Scan on public.distr2_remote d2_3
+ Output: d2_3.c, d2_3.d
+ Remote SQL: SELECT c, d FROM public.distr2_base
+ -> Hash
+ Output: d1.a, d1.b, n.n
+ -> Append
+ -> Nested Loop
+ Output: d1_1.a, d1_1.b, n.n
+ Join Filter: (d1_1.a = n.n)
+ -> Seq Scan on public.distr1 d1_1
+ Output: d1_1.a, d1_1.b
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{300,400}'::integer[])
+ -> Hash Join
+ Output: d1_2.a, d1_2.b, n.n
+ Hash Cond: (d1_2.a = n.n)
+ -> Seq Scan on public.distr1_loc d1_2
+ Output: d1_2.a, d1_2.b
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{300,400}'::integer[])
+ -> Foreign Scan
+ Output: d1_3.a, d1_3.b, n.n
+ Relations: (public.distr1_remote d1_3) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r6.a, r6.b, r3.n FROM (public.distr1_base r6 INNER JOIN unnest('{300,400}'::integer[]) r3 (n) ON (((r6.a = r3.n))))
+(36 rows)
+
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a
+ORDER BY d1.a;
+ a | b | c | d | n
+-----+-----+-----+---------+-----
+ 300 | 600 | 300 | text300 | 300
+ 400 | 800 | 400 | text400 | 400
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: d1.a, d1.b, d2.c, d2.d, n.n
+ -> Nested Loop
+ Output: d1.a, d1.b, n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{300,400}'::integer[])
+ Filter: (n.n > 3)
+ -> Append
+ -> Seq Scan on public.distr1 d1_1
+ Output: d1_1.a, d1_1.b
+ Filter: (n.n = d1_1.a)
+ -> Seq Scan on public.distr1_loc d1_2
+ Output: d1_2.a, d1_2.b
+ Filter: (n.n = d1_2.a)
+ -> Foreign Scan on public.distr1_remote d1_3
+ Output: d1_3.a, d1_3.b
+ Remote SQL: SELECT a, b FROM public.distr1_base WHERE (($1::integer = a))
+ -> Append
+ -> Seq Scan on public.distr2 d2_1
+ Output: d2_1.c, d2_1.d
+ Filter: (d1.a = d2_1.c)
+ -> Seq Scan on public.distr2_loc d2_2
+ Output: d2_2.c, d2_2.d
+ Filter: (d1.a = d2_2.c)
+ -> Foreign Scan on public.distr2_remote d2_3
+ Output: d2_3.c, d2_3.d
+ Remote SQL: SELECT c, d FROM public.distr2_base WHERE (($1::integer = c))
+(28 rows)
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3
+ORDER BY d1.a;
+ a | b | c | d | n
+-----+-----+-----+---------+-----
+ 300 | 600 | 300 | text300 | 300
+ 400 | 800 | 400 | text400 | 400
+(2 rows)
+
+DROP FOREIGN TABLE distr1_remote, distr2_remote;
+DROP TABLE distr1, distr1_loc, distr1_base, distr2, distr2_loc, distr2_base;
+-- Test UNION and function scan pushdown
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n;
+ QUERY PLAN
+----------------------------------------------------------------
+ Hash Join
+ Output: remote_tbl.a, n.n
+ Hash Cond: (remote_tbl.a = n.n)
+ -> HashAggregate
+ Output: remote_tbl.a
+ Group Key: remote_tbl.a
+ -> Append
+ -> Foreign Scan on public.remote_tbl
+ Output: remote_tbl.a
+ Remote SQL: SELECT a FROM public.base_tbl
+ -> Foreign Scan on public.remote_tbl1
+ Output: remote_tbl1.c
+ Remote SQL: SELECT c FROM public.base_tbl1
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{3,4}'::integer[])
+(18 rows)
+
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n
+ORDER BY n;
+ a | n
+---+---
+ 3 | 3
+ 4 | 4
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION ALL
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------
+ Append
+ -> Foreign Scan
+ Output: remote_tbl.a, n.n
+ Relations: (public.remote_tbl) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r6.a, r2.n FROM (public.base_tbl r6 INNER JOIN unnest('{3,4}'::integer[]) r2 (n) ON (((r6.a = r2.n))))
+ -> Foreign Scan
+ Output: remote_tbl1.c, n.n
+ Relations: (public.remote_tbl1) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r7.c, r2.n FROM (public.base_tbl1 r7 INNER JOIN unnest('{3,4}'::integer[]) r2 (n) ON (((r7.c = r2.n))))
+(9 rows)
+
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION ALL
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n
+ORDER BY n;
+ a | n
+---+---
+ 3 | 3
+ 3 | 3
+ 4 | 4
+ 4 | 4
+(4 rows)
+
+RESET enable_partitionwise_join;
+DROP FUNCTION f(INTEGER);
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 76d4fea21c4..bd6e974ebdb 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -48,6 +48,7 @@
#include "utils/rel.h"
#include "utils/sampling.h"
#include "utils/selfuncs.h"
+#include "utils/typcache.h"
PG_MODULE_MAGIC;
@@ -406,6 +407,14 @@ static void postgresGetForeignJoinPaths(PlannerInfo *root,
RelOptInfo *innerrel,
JoinType jointype,
JoinPathExtraData *extra);
+
+static void postgresTryShippableJoinPaths(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static bool postgresRecheckForeignScan(ForeignScanState *node,
TupleTableSlot *slot);
static void postgresGetForeignUpperPaths(PlannerInfo *root,
@@ -474,7 +483,7 @@ static void store_returning_result(PgFdwModifyState *fmstate,
static void finish_foreign_modify(PgFdwModifyState *fmstate);
static void deallocate_query(PgFdwModifyState *fmstate);
static List *build_remote_returning(Index rtindex, Relation rel,
- List *returningList);
+ List *returningList, Var *tid);
static void rebuild_fdw_scan_tlist(ForeignScan *fscan, List *tlist);
static void execute_dml_stmt(ForeignScanState *node);
static TupleTableSlot *get_returning_data(ForeignScanState *node);
@@ -540,6 +549,12 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
const PgFdwRelationInfo *fpinfo_i);
static int get_batch_size_option(Relation rel);
+static bool is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel);
+static void init_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ PgFdwRelationInfo *existing_fpinfo);
+
/*
* Foreign-data wrapper handler function: return a struct with pointers
@@ -595,6 +610,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
/* Support functions for join push-down */
routine->GetForeignJoinPaths = postgresGetForeignJoinPaths;
+ routine->TryShippableJoinPaths = postgresTryShippableJoinPaths;
/* Support functions for upper relation push-down */
routine->GetForeignUpperPaths = postgresGetForeignUpperPaths;
@@ -619,10 +635,31 @@ static void
postgresGetForeignRelSize(PlannerInfo *root,
RelOptInfo *baserel,
Oid foreigntableid)
+{
+ init_fpinfo(root, baserel, foreigntableid, NULL);
+}
+
+/*
+ * init_fpinfo
+ *
+ * Either initialize fpinfo based on foreign table or generate one, based on
+ * existing fpinfo.
+ * Also estimate # of rows and width of the result of the scan.
+ *
+ * We should consider the effect of all baserestrictinfo clauses here, but
+ * not any join clauses.
+ */
+static void
+init_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ PgFdwRelationInfo *existing_fpinfo)
{
PgFdwRelationInfo *fpinfo;
ListCell *lc;
- RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root);
+
+ Assert(existing_fpinfo || foreigntableid != InvalidOid);
+ Assert(existing_fpinfo == NULL || foreigntableid == InvalidOid);
/*
* We use PgFdwRelationInfo to pass various information to subsequent
@@ -634,39 +671,59 @@ postgresGetForeignRelSize(PlannerInfo *root,
/* Base foreign tables need to be pushed down always. */
fpinfo->pushdown_safe = true;
- /* Look up foreign-table catalog info. */
- fpinfo->table = GetForeignTable(foreigntableid);
- fpinfo->server = GetForeignServer(fpinfo->table->serverid);
-
- /*
- * Extract user-settable option values. Note that per-table settings of
- * use_remote_estimate, fetch_size and async_capable override per-server
- * settings of them, respectively.
- */
- fpinfo->use_remote_estimate = false;
- fpinfo->fdw_startup_cost = DEFAULT_FDW_STARTUP_COST;
- fpinfo->fdw_tuple_cost = DEFAULT_FDW_TUPLE_COST;
- fpinfo->shippable_extensions = NIL;
- fpinfo->fetch_size = 100;
- fpinfo->async_capable = false;
+ if (existing_fpinfo)
+ {
+ /* We don't have any table, related to query */
+ fpinfo->table = NULL;
+ fpinfo->server = existing_fpinfo->server;
+ }
+ else
+ {
+ /* Look up foreign-table catalog info. */
+ fpinfo->table = GetForeignTable(foreigntableid);
+ fpinfo->server = GetForeignServer(fpinfo->table->serverid);
+ }
- apply_server_options(fpinfo);
- apply_table_options(fpinfo);
+ if (existing_fpinfo)
+ {
+ merge_fdw_options(fpinfo, existing_fpinfo, NULL);
+ fpinfo->user = existing_fpinfo->user;
- /*
- * If the table or the server is configured to use remote estimates,
- * identify which user to do remote access as during planning. This
- * should match what ExecCheckRTEPerms() does. If we fail due to lack of
- * permissions, the query would have failed at runtime anyway.
- */
- if (fpinfo->use_remote_estimate)
+ /*
+ * Don't try to execute anything on remote server for
+ * non-relation-based query
+ */
+ fpinfo->use_remote_estimate = false;
+ }
+ else
{
+ RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root);
Oid userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
+ /*
+ * Extract user-settable option values. Note that per-table settings
+ * of use_remote_estimate, fetch_size and async_capable override
+ * per-server settings of them, respectively.
+ */
+ fpinfo->use_remote_estimate = false;
+ fpinfo->fdw_startup_cost = DEFAULT_FDW_STARTUP_COST;
+ fpinfo->fdw_tuple_cost = DEFAULT_FDW_TUPLE_COST;
+ fpinfo->shippable_extensions = NIL;
+ fpinfo->fetch_size = 100;
+ fpinfo->async_capable = false;
+ fpinfo->is_generated = false;
+
+ apply_server_options(fpinfo);
+ apply_table_options(fpinfo);
+
+ /*
+ * If the table or the server is configured to use remote estimates,
+ * identify which user to do remote access as during planning. This
+ * should match what ExecCheckRTEPerms() does. If we fail due to lack
+ * of permissions, the query would have failed at runtime anyway.
+ */
fpinfo->user = GetUserMapping(userid, fpinfo->server->serverid);
}
- else
- fpinfo->user = NULL;
/*
* Identify which baserestrictinfo clauses can be sent to the remote
@@ -778,6 +835,9 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->lower_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
+ if (existing_fpinfo)
+ /* Mark fpinfo generated */
+ fpinfo->is_generated = true;
}
/*
@@ -1472,13 +1532,72 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ RangeTblFunction *rtfunc;
+ TupleDesc td;
+ Oid funcrettype;
+ int num_funcs,
+ attnum;
+ ListCell *lc,
+ *lctype,
+ *lcname;
+ bool functype_OK = true;
+ List *functypes = NIL;
+
+ if (rte->funcordinality)
+ continue;
+
+ num_funcs = list_length(rte->functions);
+ Assert(num_funcs >= 0);
+
+ foreach(lc, rte->functions)
+ {
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, NULL);
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ {
+ functype_OK = false;
+ break;
+ }
+ functypes = lappend_oid(functypes, funcrettype);
+ }
+ if (!functype_OK)
+ continue;
+ td = CreateTemplateTupleDesc(num_funcs);
+
+ /*
+ * funcrettype != RECORD, so we have only one return attribute per
+ * function
+ */
+ Assert(list_length(rte->eref->colnames) == num_funcs);
+ attnum = 1;
+ forthree(lc, rte->functions, lctype, functypes, lcname, rte->eref->colnames)
+ {
+ char *colname;
+
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+ funcrettype = lfirst_oid(lctype);
+ colname = strVal(lfirst(lcname));
+
+ TupleDescInitEntry(td, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(td, (AttrNumber) attnum,
+ exprCollation(rtfunc->funcexpr));
+ attnum++;
+ }
+
+ assign_record_type_typmod(td);
+ att->atttypmod = td->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1514,15 +1633,25 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckRTEPerms() does. In case of a join or aggregate, use the
- * lowest-numbered member RTE as a representative; we would get the same
- * result from any.
+ * ExecCheckRTEPerms() does. In case of a join or aggregate, scan RTEs
+ * until RTE_RELATION is found. We would get the same result from any.
*/
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte && rte->rtekind == RTE_RELATION)
+ break;
+ }
+ Assert(rte && rte->rtekind == RTE_RELATION);
+ }
userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
/* Get info about foreign table. */
@@ -2522,8 +2651,30 @@ postgresPlanDirectModify(PlannerInfo *root,
* node below.
*/
if (fscan->scan.scanrelid == 0)
+ {
+ ListCell *lc;
+ Var *tid_var = NULL;
+
+ /*
+ * We should explicitly add tableoid to returning list if it's
+ * requested
+ */
+ foreach(lc, processed_tlist)
+ {
+ TargetEntry *tle = lfirst_node(TargetEntry, lc);
+ Var *var = (Var *) tle->expr;
+
+ if (IsA(var, Var) && (var->varattno == TableOidAttributeNumber) && (strcmp(tle->resname, "tableoid") == 0))
+ {
+ tid_var = var;
+ break;
+ }
+
+ }
+
returningList = build_remote_returning(resultRelation, rel,
- returningList);
+ returningList, tid_var);
+ }
}
/*
@@ -2848,21 +2999,65 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_relids));
rte = rt_fetch(rti, es->rtable);
- Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
- relname = get_rel_name(rte->relid);
- if (es->verbose)
+ if (rte->rtekind == RTE_RELATION)
{
- char *namespace;
-
- namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
- appendStringInfo(relations, "%s.%s",
- quote_identifier(namespace),
- quote_identifier(relname));
+ relname = get_rel_name(rte->relid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name(get_rel_namespace(rte->relid));
+ appendStringInfo(relations, "%s.%s",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfoString(relations,
+ quote_identifier(relname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ ListCell *lc;
+ int n;
+ bool first = true;
+
+
+ n = list_length(rte->functions);
+
+ if (n > 1)
+ appendStringInfo(relations, "ROWS FROM(");
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (!first)
+ appendStringInfoString(relations, ", ");
+ else
+ first = false;
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ {
+ FuncExpr *funcexpr = (FuncExpr *) rtfunc->funcexpr;
+ Oid funcid = funcexpr->funcid;
+
+ relname = get_func_name(funcid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name(get_func_namespace(funcid));
+ appendStringInfo(relations, "%s.%s()",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfo(relations, "%s()", quote_identifier(relname));
+ }
+ }
+ if (n > 1)
+ appendStringInfo(relations, ")");
}
- else
- appendStringInfoString(relations,
- quote_identifier(relname));
refname = (char *) list_nth(es->rtable_names, rti - 1);
if (refname == NULL)
refname = rte->eref->aliasname;
@@ -3201,7 +3396,7 @@ estimate_path_cost_size(PlannerInfo *root,
/* Shouldn't get here unless we have LIMIT */
Assert(fpextra->has_limit);
Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
- foreignrel->reloptkind == RELOPT_JOINREL);
+ IS_JOIN_REL(foreignrel));
startup_cost += foreignrel->reltarget->cost.startup;
run_cost += foreignrel->reltarget->cost.per_tuple * rows;
}
@@ -4378,7 +4573,7 @@ deallocate_query(PgFdwModifyState *fmstate)
* UPDATE/DELETE .. RETURNING on a join directly
*/
static List *
-build_remote_returning(Index rtindex, Relation rel, List *returningList)
+build_remote_returning(Index rtindex, Relation rel, List *returningList, Var *tid)
{
bool have_wholerow = false;
List *tlist = NIL;
@@ -4387,6 +4582,19 @@ build_remote_returning(Index rtindex, Relation rel, List *returningList)
Assert(returningList);
+ /*
+ * If tid is requested, add it to the returning list
+ */
+ if (tid)
+ {
+ tlist = lappend(tlist,
+ makeTargetEntry((Expr *) tid,
+ list_length(tlist) + 1,
+ NULL,
+ false));
+
+ }
+
vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
/*
@@ -4679,11 +4887,13 @@ init_returning_filter(PgFdwDirectModifyState *dmstate,
if (attrno < 0)
{
/*
- * We don't retrieve system columns other than ctid and oid.
+ * We don't retrieve system columns other than ctid and oid,
+ * but locally-generated tableoid can appear in returning
+ * list.
*/
if (attrno == SelfItemPointerAttributeNumber)
dmstate->ctidAttno = i;
- else
+ else if (attrno != TableOidAttributeNumber)
Assert(false);
dmstate->hasSystemCols = true;
}
@@ -5487,6 +5697,128 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Determine if foreignrel, not backed by foreign
+ * table, is fine to push down.
+ */
+static bool
+is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel)
+{
+ RangeTblEntry *rte;
+ RangeTblFunction *rtfunc;
+
+ rte = planner_rt_fetch(foreignrel->relid, root);
+
+ if (!rte)
+ return false;
+
+ Assert(foreignrel->fdw_private);
+
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ ListCell *lc;
+
+ Assert(list_length(rte->functions) >= 1);
+ foreach(lc, rte->functions)
+ {
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (contain_var_clause(rtfunc->funcexpr) ||
+ contain_mutable_functions(rtfunc->funcexpr) ||
+ contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, foreignrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Check if reltarget is safe enough to push down such join
+ */
+static bool
+joinrel_target_ok(PlannerInfo *root, RelOptInfo *joinrel)
+{
+ List *vars;
+ ListCell *lc;
+ bool ok = true;
+
+ Assert(joinrel->reltarget);
+
+ /* TODO: is flag correct ? */
+ vars = pull_var_clause((Node *) joinrel->reltarget->exprs, PVC_INCLUDE_PLACEHOLDERS);
+
+ foreach(lc, vars)
+ {
+ Var *var = (Var *) lfirst(lc);
+
+ /*
+ * We can't return abstract records, forbid such foreign joins, except
+ * cases when we can derive its type
+ */
+ if (IsA(var, Var) &&
+ var->vartype == RECORDOID &&
+ var->vartypmod < 0)
+ {
+ ok = false;
+
+ if (var->varattno == InvalidAttrNumber)
+ {
+ RangeTblEntry *rte = planner_rt_fetch(var->varno, root);
+
+ if (rte)
+ {
+ if (rte->rtekind == RTE_RELATION)
+ {
+ Oid reltype;
+
+ reltype = get_rel_type_id(rte->relid);
+ if (OidIsValid(reltype))
+ ok = true;
+ }
+ else if (rte->rtekind == RTE_FUNCTION && !rte->funcordinality)
+ {
+ ListCell *lc;
+ int n;
+
+ n = list_length(rte->functions);
+
+ Assert(n >= 1);
+
+ if (n == list_length(rte->eref->colnames))
+ {
+ ok = true;
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc;
+ TupleDesc tupdesc;
+ Oid funcrettype;
+
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID || funcrettype == VOIDOID)
+ {
+ ok = false;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!ok)
+ break;
+ }
+ }
+ return ok;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -5512,6 +5844,12 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
jointype != JOIN_RIGHT && jointype != JOIN_FULL)
return false;
+ /*
+ * We can't push down join if its reltarget is not safe
+ */
+ if (!joinrel_target_ok(root, joinrel))
+ return false;
+
/*
* If either of the joining relations is marked as unsafe to pushdown, the
* join can not be pushed down.
@@ -6075,6 +6413,43 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
/* XXX Consider parameterized paths for the join relation */
}
+/*
+ * postgresTryShippableJoinPaths
+ *
+ * Try to add foreign join of foreign relation with shippable RTE.
+ */
+static void
+postgresTryShippableJoinPaths(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra)
+{
+ PgFdwRelationInfo *fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ PgFdwRelationInfo *fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+
+ if (fpinfo_o == NULL)
+ /* Outer path is not foreign relation or foreign JOIN. */
+ return;
+
+ if (joinrel->fdwroutine != NULL || innerrel->reloptkind != RELOPT_BASEREL)
+ return;
+
+ if (fpinfo_i == NULL || fpinfo_i->is_generated)
+ init_fpinfo(root, innerrel, InvalidOid, fpinfo_o);
+
+ if (!is_nonrel_relinfo_ok(root, innerrel))
+ return;
+
+ joinrel->serverid = outerrel->serverid;
+ joinrel->userid = outerrel->userid;
+ joinrel->useridiscurrent = outerrel->useridiscurrent;
+ joinrel->fdwroutine = outerrel->fdwroutine;
+
+ postgresGetForeignJoinPaths(root, joinrel, outerrel, innerrel, jointype, extra);
+}
+
/*
* Assess whether the aggregation, grouping and having operations can be pushed
* down to the foreign server. As a side effect, save information we obtain in
@@ -6505,7 +6880,7 @@ add_foreign_ordered_paths(PlannerInfo *root, RelOptInfo *input_rel,
* standard_qp_callback()).
*/
if (input_rel->reloptkind == RELOPT_BASEREL ||
- input_rel->reloptkind == RELOPT_JOINREL)
+ IS_JOIN_REL(input_rel))
{
Assert(root->query_pathkeys == root->sort_pathkeys);
@@ -6656,11 +7031,11 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel,
* join relation.
*/
Assert(input_rel->reloptkind == RELOPT_BASEREL ||
- input_rel->reloptkind == RELOPT_JOINREL ||
+ IS_JOIN_REL(input_rel) ||
(input_rel->reloptkind == RELOPT_UPPER_REL &&
ifpinfo->stage == UPPERREL_ORDERED &&
(ifpinfo->outerrel->reloptkind == RELOPT_BASEREL ||
- ifpinfo->outerrel->reloptkind == RELOPT_JOINREL)));
+ IS_JOIN_REL(ifpinfo->outerrel))));
foreach(lc, input_rel->pathlist)
{
@@ -6728,7 +7103,7 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel,
/* The input_rel should be a base, join, or grouping relation */
Assert(input_rel->reloptkind == RELOPT_BASEREL ||
- input_rel->reloptkind == RELOPT_JOINREL ||
+ IS_JOIN_REL(input_rel) ||
(input_rel->reloptkind == RELOPT_UPPER_REL &&
ifpinfo->stage == UPPERREL_GROUP_AGG));
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 90b72e9ec55..0f599cdef5a 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -52,6 +52,12 @@ typedef struct PgFdwRelationInfo
/* True means that the query_pathkeys is safe to push down */
bool qp_is_pushdown_safe;
+ /*
+ * True means that PgFdwRelationInfo is not extracted from catalogs, but
+ * generated
+ */
+ bool is_generated;
+
/* Cost and selectivity of local_conds. */
QualCost local_conds_cost;
Selectivity local_conds_sel;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 38f4a7837fe..fb3469efe5a 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -3422,3 +3422,339 @@ CREATE FOREIGN TABLE inv_fsz (c1 int )
-- Invalid batch_size option
CREATE FOREIGN TABLE inv_bsz (c1 int )
SERVER loopback OPTIONS (batch_size '100$%$#$#');
+
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*, t.n from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), json_each_text('{"a":"text1", "c":"text4"}')) t (n, k, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+
+SELECT r.*, t.txt from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), json_each_text('{"a":"text1", "c":"text4"}')) t (n, k, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+
+-- complex types
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx);
+
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx)
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') ON d = value;
+
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') ON d = value
+ORDER BY r1.c;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') AS t(u,v) ON d = v;
+
+SELECT * FROM remote_tbl1 r1 JOIN json_each_text('{"a":"text1", "b":2, "c":"text14"}') AS t(u,v) ON d = v
+ORDER BY r1.c;
+
+-- DML
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+
+-- Test joins with append relations
+
+SET enable_partitionwise_join=on;
+
+-- Partitioned tables and function scan pushdown
+
+CREATE TABLE distr1(a int, b int) PARTITION BY HASH(a);
+CREATE TABLE distr1_part_1 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 0);
+CREATE TABLE distr1_base_2 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_2 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr1_base_2', use_remote_estimate 'true');
+CREATE TABLE distr1_base_3 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_3 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 2)
+ SERVER loopback2 OPTIONS (table_name 'distr1_base_3', use_remote_estimate 'true');
+CREATE TABLE distr1_base_4 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_4 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 4, REMAINDER 3)
+ SERVER loopback OPTIONS (table_name 'distr1_base_4', use_remote_estimate 'true');
+
+CREATE TABLE distr2(c int, d text) PARTITION BY HASH(c);
+CREATE TABLE distr2_part_1 PARTITION OF distr2 FOR VALUES WITH (MODULUS 4, REMAINDER 0);
+CREATE TABLE distr2_base_2 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_2 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr2_base_2', use_remote_estimate 'true');
+CREATE TABLE distr2_base_3 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_3 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 2)
+ SERVER loopback2 OPTIONS (table_name 'distr2_base_3', use_remote_estimate 'true');
+CREATE TABLE distr2_base_4 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_4 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 4, REMAINDER 3)
+ SERVER loopback OPTIONS (table_name 'distr2_base_4', use_remote_estimate 'true');
+
+INSERT INTO distr1 SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO distr2 SELECT g, 'text'|| g from generate_series(1,500) g;
+
+ANALYZE distr1;
+ANALYZE distr1_part_1;
+ANALYZE distr1_base_2;
+ANALYZE distr1_base_3;
+ANALYZE distr1_base_4;
+ANALYZE distr2;
+ANALYZE distr2_base_2;
+ANALYZE distr2_base_3;
+ANALYZE distr2_base_4;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n;
+
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n
+ORDER BY d.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n;
+
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n
+ORDER BY d.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a;
+
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a
+ORDER BY d1.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3;
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3
+ORDER BY d1.a;
+
+-- Direct update with returning
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+
+-- Direct update with returning tableoid
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b,tableoid;
+
+UPDATE distr1 d1 SET b=t.l FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b,tableoid;
+
+-- Indirect update with returning
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE distr1 d1 SET b=CASE WHEN random()>=0 THEN t.l ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE distr1 d1 SET b=CASE WHEN random()>=0 THEN t.l ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE d1.a between l and area(t.bx)
+RETURNING a,b;
+
+DROP TABLE distr1, distr2, distr1_base_2, distr2_base_2, distr1_base_3, distr2_base_3, distr1_base_4, distr2_base_4;
+
+-- Test pushdown of several function scans
+
+CREATE TABLE distr1(a int, b int) PARTITION BY HASH(a);
+CREATE TABLE distr1_part_1 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 2, REMAINDER 0);
+CREATE TABLE distr1_base_2 (a int, b int);
+CREATE FOREIGN TABLE distr1_part_2 PARTITION OF distr1 FOR VALUES WITH ( MODULUS 2, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr1_base_2', use_remote_estimate 'true');
+
+CREATE TABLE distr2(c int, d text) PARTITION BY HASH(c);
+CREATE TABLE distr2_part_1 PARTITION OF distr2 FOR VALUES WITH (MODULUS 2, REMAINDER 0);
+CREATE TABLE distr2_base_2 (c int, d text);
+CREATE FOREIGN TABLE distr2_part_2 PARTITION OF distr2 FOR VALUES WITH ( MODULUS 2, REMAINDER 1)
+ SERVER loopback OPTIONS (table_name 'distr2_base_2', use_remote_estimate 'true');
+
+INSERT INTO distr1 SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO distr2 SELECT g, 'text'|| g from generate_series(1,500) g;
+
+ANALYZE;
+
+-- Make local function scan not so attractive
+ALTER SERVER loopback OPTIONS (ADD fdw_tuple_cost '1000');
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr1 d2, unnest(array[3,4]) n, unnest(array[3,4]) g
+WHERE d1.a = n AND d2.a = d1.a and g = n;
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[3,4]) n, generate_series(1,10000) g
+WHERE d1.a = n AND d2.c = d1.a and g = d1.a
+ORDER BY d1.a;
+
+ALTER SERVER loopback OPTIONS (DROP fdw_tuple_cost);
+
+DROP TABLE distr1, distr2, distr1_base_2, distr2_base_2;
+
+-- Test inheritance chain and function scan pushdown
+
+CREATE TABLE distr1(a int, b int);
+CREATE TABLE distr1_loc (a int, b int) INHERITS (distr1);
+CREATE TABLE distr1_base (a int, b int, c text);
+CREATE FOREIGN TABLE distr1_remote (a int, b int, c text) INHERITS (distr1)
+ SERVER loopback OPTIONS (table_name 'distr1_base', use_remote_estimate 'true');
+
+CREATE TABLE distr2(c int, d text);
+CREATE TABLE distr2_loc (c int, d text) INHERITS (distr2);
+CREATE TABLE distr2_base (c int, d text, e int);
+CREATE FOREIGN TABLE distr2_remote (c int, d text, e int) INHERITS (distr2)
+ SERVER loopback OPTIONS (table_name 'distr2_base', use_remote_estimate 'true');
+
+INSERT INTO distr1_loc SELECT g, g*2 from generate_series(1,100) g;
+INSERT INTO distr1_base SELECT g, g*2, 'text'|| g from generate_series(200,20000) g;
+
+INSERT INTO distr2_loc SELECT g, 'text'|| g from generate_series(1,100) g;
+INSERT INTO distr2_base SELECT g, 'text'|| g, g*2 from generate_series(200,20000) g;
+
+ANALYZE distr1, distr1_loc, distr1_base;
+ANALYZE distr2, distr2_loc, distr2_base;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n;
+
+SELECT * FROM distr1 d, unnest(array[2,3,4,5]) n WHERE d.a = n
+ORDER BY d.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n;
+
+SELECT * FROM unnest(array[2,3,4,5]) n, distr1 d WHERE d.a = n
+ORDER BY d.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a;
+
+SELECT * FROM distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a
+ORDER BY d1.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * from distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3;
+
+SELECT * from distr1 d1, distr2 d2, unnest(array[300,400]) n
+WHERE d1.a = n AND d2.c = d1.a and n > 3
+ORDER BY d1.a;
+
+DROP FOREIGN TABLE distr1_remote, distr2_remote;
+DROP TABLE distr1, distr1_loc, distr1_base, distr2, distr2_loc, distr2_base;
+
+-- Test UNION and function scan pushdown
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n;
+
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n
+ORDER BY n;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION ALL
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n;
+
+SELECT * FROM
+(SELECT a FROM remote_tbl
+UNION ALL
+SELECT c FROM remote_tbl1) u
+JOIN unnest(array[3,4]) n
+ON u.a = n
+ORDER BY n;
+
+RESET enable_partitionwise_join;
+
+DROP FUNCTION f(INTEGER);
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 32618ebbd51..c8b12c9360d 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -329,6 +329,17 @@ add_paths_to_joinrel(PlannerInfo *root,
outerrel, innerrel,
jointype, &extra);
+ /*
+ * If push down of join is not possible we can try to join foreign
+ * relation with shippable RTE. In this case we have a chance to push down
+ * this join yet.
+ */
+ else if (outerrel->fdwroutine &&
+ outerrel->fdwroutine->TryShippableJoinPaths)
+ outerrel->fdwroutine->TryShippableJoinPaths(root, joinrel,
+ outerrel, innerrel,
+ jointype, &extra);
+
/*
* 6. Finally, give extensions a chance to manipulate the path list.
*/
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 02529da562b..172122ed4d9 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -500,7 +500,6 @@ add_rte_to_flat_rtable(PlannerGlobal *glob, RangeTblEntry *rte)
newrte->joinleftcols = NIL;
newrte->joinrightcols = NIL;
newrte->join_using_alias = NULL;
- newrte->functions = NIL;
newrte->tablefunc = NULL;
newrte->values_lists = NIL;
newrte->coltypes = NIL;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index ddf0f5a8765..9675aa6b3ab 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -22,12 +22,14 @@
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
+#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/placeholder.h"
#include "optimizer/plancat.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/tlist.h"
+#include "parser/parsetree.h"
#include "utils/hsearch.h"
#include "utils/lsyscache.h"
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index a801cd30576..58555094da3 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -221,6 +221,7 @@ typedef struct FdwRoutine
/* Functions for remote-join planning */
GetForeignJoinPaths_function GetForeignJoinPaths;
+ GetForeignJoinPaths_function TryShippableJoinPaths;
/* Functions for remote upper-relation (post scan/join) planning */
GetForeignUpperPaths_function GetForeignUpperPaths;
--
2.25.1
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2021-05-20 17:43 Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2021-06-15 13:15 ` Re: Function scan FDW pushdown Ashutosh Bapat <[email protected]>
2021-10-04 07:42 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
@ 2024-11-05 16:11 ` [email protected]
2025-08-05 21:32 ` Re: Function scan FDW pushdown Álvaro Herrera <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: [email protected] @ 2024-11-05 16:11 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Alexander Pyhalov писал(а) 2021-10-04 10:42:
> Ashutosh Bapat писал 2021-06-15 16:15:
>> Hi Alexander,
>
> Hi.
>
> The current version of the patch is based on asymetric partition-wise
> join.
> Currently it is applied after
> v19-0001-Asymmetric-partitionwise-join.patch from
> on
> https://www.postgresql.org/message-id/[email protected]
> .
>
>>> So far I don't know how to visualize actual function expression used
>>> in
>>> function RTE, as in postgresExplainForeignScan() es->rtable comes
>>> from
>>> queryDesc->plannedstmt->rtable, and rte->functions is already 0.
>>
>> The actual function expression will be part of the Remote SQL of
>> ForeignScan node so no need to visualize it separately.
>
> We still need to create tuple description for functions in
> get_tupdesc_for_join_scan_tuples(),
> so I had to remove setting newrte->functions to NIL in
> add_rte_to_flat_rtable().
> With rte->functions in place, there's no issues for explain.
>
>>
>> The patch will have problems when there are multiple foreign tables
>> all on different servers or use different FDWs. In such a case the
>> function scan's RelOptInfo will get the fpinfo based on the first
>> foreign table the function scan is paired with during join planning.
>> But that may not be the best foreign table to join. We should be able
>> to plan all the possible joins. Current infra to add one fpinfo per
>> RelOptInfo won't help there. We need something better.
>
> I suppose attached version of the patch is more mature.
>
>>
>> The patch targets only postgres FDW, how do you see this working with
>> other FDWs?
>
> Not now. We introduce necessary APIs for other FDWs, but implementing
> TryShippableJoinPaths()
> doesn't seem straightforward.
>
>>
>> If we come up with the right approach we could use it for 1. pushing
>> down queries with IN () clause 2. joining a small local table with a
>> large foreign table by sending the local table rows down to the
>> foreign server.
Hi,
This is a long-overdue follow-up to the original patch.
Note that this patch contains only the changes required for
function scan pushdown, examples and code related to asymmetric
join are dropped.
In this version, we have fixed some of the issues:
1) Functions returning records are now deparsed correctly
2) I have benchmarked this version alongside the current master
using HammerDB and it shows a consistent ~6000 number of orders
per minute (NOPM) compared to ~100 NOPM on local setup with
1 virtual user and 10 warehouses
The benchmarking was concluded in the following way:
1) schema was created and filled up by buildschema on the first instance
2) foreign schema was imported on the second instance
3) functions created by buildschema (all in namespace 'public') were
exported from the first instance and created on the second one
4) HammerDB was connected to the second instance, and the benchmark
was run there
Note that the HammerDB service functions that were imported to the
second
instance are not the ones being pushed down, only PostgreSQL built-ins
are.
The issue with setting newrte->functions to NIL still persists.
I've attempted to work around it by adding the rte->functions
list to fdw_private field. I stored a copy of rte->functions for each
RTE in the order they are listed in simple_rte_array and accessed it
where the rte->functions were used in the original patch. While this
being pretty straightforward, I found out the hard way that the RTEs
are flattened and the original sequential order known at
postgresGetForeignPlan() and postgresPlanDirectModify() is altered
randomly. It prevents me from looking up the right functions list for
the RTE by its rti in postgresExplainForeignScan() and its var->varno in
get_tupdesc_for_join_scan_tuples(). I am aware that the rte->functions
will now be copied even on instances that don't utilize a FDW, but I
don't see a way to solve it. Any suggestions are welcome.
Best regards,
Gleb Kashkin,
Postgres Professional
Attachments:
[text/x-diff] 0001-Push-join-with-function-scan-to-remote-server.patch (52.0K, ../../[email protected]/2-0001-Push-join-with-function-scan-to-remote-server.patch)
download | inline diff:
From a59b04e33147a075fdd4c6cef155440689850b83 Mon Sep 17 00:00:00 2001
From: Alexander Pyhalov <[email protected]>
Date: Mon, 17 May 2021 19:19:31 +0300
Subject: [PATCH] Push join with function scan to remote server
The patch allows pushing joins with function RTEs to PostgreSQL
data sources in general and postgres_fdw specifically.
Co-authored-by: Gleb Kashkin <[email protected]>
---
contrib/postgres_fdw/deparse.c | 187 ++++++--
.../postgres_fdw/expected/postgres_fdw.out | 368 +++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 423 +++++++++++++++---
contrib/postgres_fdw/postgres_fdw.h | 6 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 148 ++++++
src/backend/optimizer/path/joinpath.c | 11 +
src/backend/optimizer/plan/setrefs.c | 1 -
src/include/foreign/fdwapi.h | 1 +
src/test/regress/expected/create_view.out | 6 +-
9 files changed, 1046 insertions(+), 105 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 4680d51733..17228dec47 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -153,6 +153,7 @@ static void deparseConst(Const *node, deparse_expr_cxt *context, int showtype);
static void deparseParam(Param *node, deparse_expr_cxt *context);
static void deparseSubscriptingRef(SubscriptingRef *node, deparse_expr_cxt *context);
static void deparseFuncExpr(FuncExpr *node, deparse_expr_cxt *context);
+static void deparseFuncColnames(StringInfo buf, int varno, RangeTblEntry *rte, bool qualify_col);
static void deparseOpExpr(OpExpr *node, deparse_expr_cxt *context);
static bool isPlainForeignVar(Expr *node, deparse_expr_cxt *context);
static void deparseOperatorName(StringInfo buf, Form_pg_operator opform);
@@ -1977,13 +1978,54 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
{
RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
- /*
- * Core code already has some lock on each rel being planned, so we
- * can use NoLock here.
- */
- Relation rel = table_open(rte->relid, NoLock);
+ Assert(rte->rtekind == RTE_RELATION || rte->rtekind == RTE_FUNCTION);
+ if (rte->rtekind == RTE_RELATION)
+ {
+ /*
+ * Core code already has some lock on each rel being planned, so
+ * we can use NoLock here.
+ */
+ Relation rel = table_open(rte->relid, NoLock);
- deparseRelation(buf, rel);
+ deparseRelation(buf, rel);
+
+ table_close(rel, NoLock);
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ RangeTblFunction *rtfunc;
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool first = true;
+ int n;
+
+ n = list_length(rte->functions);
+ Assert(n >= 1);
+
+ if (n > 1)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ if (!first)
+ appendStringInfoString(buf, ", ");
+ else
+ first = false;
+
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ context.root = root;
+ context.foreignrel = foreignrel;
+ context.scanrel = foreignrel;
+ context.buf = buf;
+ context.params_list = params_list;
+
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ }
+
+ if (n > 1)
+ appendStringInfoString(buf, ")");
+ }
/*
* Add a unique alias to avoid any conflict in relation names due to
@@ -1991,9 +2033,43 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
* join.
*/
if (use_alias)
+ {
appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ appendStringInfo(buf, " (");
+ deparseFuncColnames(buf, 0, rte, false);
+ appendStringInfo(buf, ") ");
+ }
+ }
+ }
+}
- table_close(rel, NoLock);
+/*
+ * Deparse function columns alias list
+ */
+static void
+deparseFuncColnames(StringInfo buf, int varno, RangeTblEntry *rte, bool qualify_col)
+{
+ bool first = true;
+ ListCell *lc;
+
+ Assert(rte);
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(rte->eref);
+
+ foreach(lc, rte->eref->colnames)
+ {
+ char *colname = strVal(lfirst(lc));
+
+ if (colname[0] == '\0')
+ continue;
+ if (!first)
+ appendStringInfoString(buf, ",");
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
+ appendStringInfoString(buf, quote_identifier(colname));
+ first = false;
}
}
@@ -2717,23 +2793,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
/* Required only to be passed down to deparseTargetList(). */
List *retrieved_attrs;
- /*
- * The lock on the relation will be held by upper callers, so it's
- * fine to open it with no lock here.
- */
- rel = table_open(rte->relid, NoLock);
-
- /*
- * The local name of the foreign table can not be recognized by the
- * foreign server and the table it references on foreign server might
- * have different column ordering or different columns than those
- * declared locally. Hence we have to deparse whole-row reference as
- * ROW(columns referenced locally). Construct this by deparsing a
- * "whole row" attribute.
- */
- attrs_used = bms_add_member(NULL,
- 0 - FirstLowInvalidHeapAttributeNumber);
-
/*
* In case the whole-row reference is under an outer join then it has
* to go NULL whenever the rest of the row goes NULL. Deparsing a join
@@ -2748,16 +2807,43 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
}
appendStringInfoString(buf, "ROW(");
- deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
- &retrieved_attrs);
+ if (rte->rtekind == RTE_RELATION)
+ {
+ /*
+ * The local name of the foreign table can not be recognized by
+ * the foreign server and the table it references on foreign
+ * server might have different column ordering or different
+ * columns than those declared locally. Hence we have to deparse
+ * whole-row reference as ROW(columns referenced locally).
+ * Construct this by deparsing a "whole row" attribute.
+ */
+ attrs_used = bms_add_member(NULL,
+ 0 - FirstLowInvalidHeapAttributeNumber);
+
+ /*
+ * The lock on the relation will be held by upper callers, so it's
+ * fine to open it with no lock here.
+ */
+ rel = table_open(rte->relid, NoLock);
+ deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
+ &retrieved_attrs);
+ table_close(rel, NoLock);
+ bms_free(attrs_used);
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * Function call is translated as-is, function returns the same
+ * columns in the same order as on local server
+ */
+ deparseFuncColnames(buf, varno, rte, qualify_col);
+ }
appendStringInfoChar(buf, ')');
/* Complete the CASE WHEN statement started above. */
if (qualify_col)
appendStringInfoString(buf, " END");
- table_close(rel, NoLock);
- bms_free(attrs_used);
}
else
{
@@ -2772,29 +2858,40 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
* If it's a column of a foreign table, and it has the column_name FDW
* option, use that value.
*/
- options = GetForeignColumnOptions(rte->relid, varattno);
- foreach(lc, options)
+ if (rte->rtekind == RTE_RELATION)
{
- DefElem *def = (DefElem *) lfirst(lc);
-
- if (strcmp(def->defname, "column_name") == 0)
+ options = GetForeignColumnOptions(rte->relid, varattno);
+ foreach(lc, options)
{
- colname = defGetString(def);
- break;
+ DefElem *def = (DefElem *) lfirst(lc);
+
+ if (strcmp(def->defname, "column_name") == 0)
+ {
+ colname = defGetString(def);
+ break;
+ }
}
- }
- /*
- * If it's a column of a regular table or it doesn't have column_name
- * FDW option, use attribute name.
- */
- if (colname == NULL)
- colname = get_attname(rte->relid, varattno, false);
+ /*
+ * If it's a column of a regular table or it doesn't have
+ * column_name FDW option, use attribute name.
+ */
+ if (colname == NULL)
+ colname = get_attname(rte->relid, varattno, false);
- if (qualify_col)
- ADD_REL_QUALIFIER(buf, varno);
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
- appendStringInfoString(buf, quote_identifier(colname));
+ appendStringInfoString(buf, quote_identifier(colname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ colname = get_rte_attribute_name(rte, varattno);
+
+ if (qualify_col)
+ ADD_REL_QUALIFIER(buf, varno);
+ appendStringInfoString(buf, quote_identifier(colname));
+ }
}
}
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f2bcd6aa98..686ce1ac61 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12345,3 +12345,371 @@ SELECT CASE WHEN closed IS NOT false THEN 1 ELSE 0 END
-- Clean up
\set VERBOSITY default
RESET debug_discard_caches;
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r2.n FROM (public.base_tbl r1 INNER JOIN unnest('{2,3,4}'::integer[]) r2 (n) ON (((r1.a = r2.n))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+ a | b | n
+---+---+---
+ 2 | 4 | 2
+ 3 | 6 | 3
+ 4 | 8 | 4
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.n, r2.a, r2.b FROM (public.base_tbl r2 INNER JOIN unnest('{2,3,4}'::integer[]) r1 (n) ON (((r2.a = r1.n))))
+(4 rows)
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+ n | a | b
+---+---+---
+ 2 | 2 | 4
+ 3 | 3 | 6
+ 4 | 4 | 8
+(3 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, r1.c, r1.d, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r2.c, r2.d, r3.n FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r1.a = r3.n))))
+(4 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a
+ORDER BY r.a;
+ a | b | c | d | n
+---+---+---+-------+---
+ 3 | 6 | 3 | text3 | 3
+ 4 | 8 | 4 | text4 | 4
+(2 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, n.n
+ Relations: ((public.remote_tbl r) INNER JOIN (public.remote_tbl1 r1)) INNER JOIN (pg_catalog.unnest() n)
+ Remote SQL: SELECT r1.a, r1.b, r3.n FROM ((public.base_tbl r1 INNER JOIN public.base_tbl1 r2 ON (((r1.a = r2.c)))) INNER JOIN unnest('{3,4}'::integer[]) r3 (n) ON (((r1.a = r3.n)) AND ((r3.n > 3))))
+(4 rows)
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+CREATE OR REPLACE FUNCTION get_constant_texts()
+RETURNS TABLE (text_value text) AS $$
+BEGIN
+ RETURN QUERY VALUES
+ ('text1'),
+ ('text4');
+END;
+$$ LANGUAGE plpgsql IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION get_constant_texts();
+ALTER SERVER loopback OPTIONS (extensions 'postgres_fdw');
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*, t.n from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), get_constant_texts()) t (n, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.c, r.d, t.n
+ Relations: (public.remote_tbl1 r) INNER JOIN (ROWS FROM(pg_catalog.unnest(), public.get_constant_texts()) t)
+ Remote SQL: SELECT r1.c, r1.d, r2.n FROM (public.base_tbl1 r1 INNER JOIN ROWS FROM (unnest('{3,4}'::integer[]), public.get_constant_texts()) r2 (n,txt) ON (((r1.c = r2.n)) AND ((r2.txt = r1.d))))
+(4 rows)
+
+SELECT r.*, t.txt from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), get_constant_texts()) t (n, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+ c | d | txt
+---+-------+-------
+ 4 | text4 | text4
+(1 row)
+
+-- Complex types
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx);
+ QUERY PLAN
+---------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: r.a, r.b, t.bx
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() t)
+ Remote SQL: SELECT r1.a, r1.b, r2.bx FROM (public.base_tbl r1 INNER JOIN unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) ON (((r1.a = area(r2.bx)))))
+(4 rows)
+
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx)
+ORDER BY r.a;
+ a | b | bx
+----+----+---------------
+ 24 | 48 | (2,3),(-2,-3)
+(1 row)
+
+-- DML
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.base_tbl r1 SET b = 5 FROM unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) WHERE ((r1.a = area(r2.bx))) RETURNING r1.a, r1.b
+(4 rows)
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
+ -> Foreign Scan
+ Output: CASE WHEN (random() >= '0'::double precision) THEN 5 ELSE 0 END, r.ctid, r.*, t.*
+ Relations: (public.remote_tbl r) INNER JOIN (pg_catalog.unnest() t)
+ Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.a, r1.b) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.bx) END FROM (public.base_tbl r1 INNER JOIN unnest('{(2,3),(-2,-3)}'::box[]) r2 (bx) ON (((r1.a = area(r2.bx))))) FOR UPDATE OF r1
+ -> Hash Join
+ Output: r.ctid, r.*, t.*
+ Hash Cond: ((r.a)::double precision = area(t.bx))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.ctid, r.*, r.a
+ Remote SQL: SELECT a, b, ctid FROM public.base_tbl FOR UPDATE
+ -> Hash
+ Output: t.*, t.bx
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.*, t.bx
+ Function Call: unnest('{(2,3),(-2,-3)}'::box[])
+(18 rows)
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ -> Foreign Update
+ Remote SQL: UPDATE public.base_tbl r1 SET b = 5 FROM ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) WHERE ((r1.a >= r2.l)) AND ((r1.a <= area(r2.bx))) RETURNING r1.a, r1.b
+(4 rows)
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 10 | 5
+ 11 | 5
+ 12 | 5
+ 13 | 5
+ 14 | 5
+ 15 | 5
+ 16 | 5
+ 17 | 5
+ 18 | 5
+ 19 | 5
+ 20 | 5
+ 21 | 5
+ 22 | 5
+ 23 | 5
+ 25 | 5
+ 26 | 5
+ 27 | 5
+ 28 | 5
+ 24 | 5
+(19 rows)
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: UPDATE public.base_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
+ -> Foreign Scan
+ Output: CASE WHEN (random() >= '0'::double precision) THEN 5 ELSE 0 END, r.ctid, r.*, t.*
+ Relations: (public.remote_tbl r) INNER JOIN (ROWS FROM(pg_catalog.unnest(), pg_catalog.unnest()) t)
+ Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.a, r1.b) END, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2.l,r2.bx) END FROM (public.base_tbl r1 INNER JOIN ROWS FROM (unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])) r2 (l,bx) ON (((r1.a >= r2.l)) AND ((r1.a <= area(r2.bx))))) FOR UPDATE OF r1
+ -> Nested Loop
+ Output: r.ctid, r.*, t.*
+ Join Filter: ((r.a >= t.l) AND ((r.a)::double precision <= area(t.bx)))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.ctid, r.*, r.a
+ Remote SQL: SELECT a, b, ctid FROM public.base_tbl FOR UPDATE
+ -> Function Scan on t
+ Output: t.*, t.l, t.bx
+ Function Call: unnest('{10,20}'::integer[]), unnest('{(2,3),(-2,-4);(1,2),(-2,-3)}'::box[])
+(16 rows)
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+ a | b
+----+---
+ 10 | 5
+ 11 | 5
+ 12 | 5
+ 13 | 5
+ 14 | 5
+ 15 | 5
+ 16 | 5
+ 17 | 5
+ 18 | 5
+ 19 | 5
+ 20 | 5
+ 21 | 5
+ 22 | 5
+ 23 | 5
+ 25 | 5
+ 26 | 5
+ 27 | 5
+ 28 | 5
+ 24 | 5
+(19 rows)
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: r.a, r.b, (generate_series(1, 10))
+ Hash Cond: (r.a = (generate_series(1, 10)))
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl
+ -> Hash
+ Output: (generate_series(1, 10))
+ -> ProjectSet
+ Output: generate_series(1, 10)
+ -> Result
+(11 rows)
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+ a | b | c | d | n
+---+---+---+-------+---
+ 4 | 8 | 4 | text4 | 4
+(1 row)
+
+-- Test that a function that returns a record is not pushed down
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$ SELECT (1,2)::record $$ language SQL IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD function f_ret_record();
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+-----------------------------------------------------------------------------
+ Nested Loop
+ Output: s.*
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl WHERE ((a = $1::integer))
+(8 rows)
+
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ s
+-------
+ (1,2)
+(1 row)
+
+DROP FUNCTION f(INTEGER);
+ALTER EXTENSION postgres_fdw DROP FUNCTION get_constant_texts();
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ret_record();
+DROP FUNCTION get_constant_texts();
+DROP FUNCTION f_ret_record();
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
+-- Test that function WITH ORDINALITY is not pushed down
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int) SERVER loopback OPTIONS (table_name 'base_tbl');
+INSERT INTO remote_tbl VALUES (1, 2), (2, 3), (3, 4), (5, 6);
+ANALYZE remote_tbl;
+SET enable_mergejoin TO false;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl, unnest(ARRAY[1, 2]) WITH ORDINALITY
+WHERE a=unnest;
+ QUERY PLAN
+------------------------------------------------------------------------
+ Hash Join
+ Output: remote_tbl.a, remote_tbl.b, unnest.unnest, unnest.ordinality
+ Hash Cond: (remote_tbl.a = unnest.unnest)
+ -> Foreign Scan on public.remote_tbl
+ Output: remote_tbl.a, remote_tbl.b
+ Remote SQL: SELECT a, b FROM public.base_tbl
+ -> Hash
+ Output: unnest.unnest, unnest.ordinality
+ -> Function Scan on pg_catalog.unnest
+ Output: unnest.unnest, unnest.ordinality
+ Function Call: unnest('{1,2}'::integer[])
+(11 rows)
+
+SELECT * FROM remote_tbl, unnest(ARRAY[1, 2]) WITH ORDINALITY
+WHERE a=unnest;
+ a | b | unnest | ordinality
+---+---+--------+------------
+ 1 | 2 | 1 | 1
+ 2 | 3 | 2 | 2
+(2 rows)
+
+DROP TABLE base_tbl;
+DROP FOREIGN TABLE remote_tbl;
+RESET enable_mergejoin;
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 53733d642d..5d0a339ca0 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -36,6 +36,7 @@
#include "optimizer/prep.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/tlist.h"
+#include "optimizer/clauses.h"
#include "parser/parsetree.h"
#include "postgres_fdw.h"
#include "storage/latch.h"
@@ -406,6 +407,14 @@ static void postgresGetForeignJoinPaths(PlannerInfo *root,
RelOptInfo *innerrel,
JoinType jointype,
JoinPathExtraData *extra);
+
+static void postgresTryShippableJoinPaths(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra);
+
static bool postgresRecheckForeignScan(ForeignScanState *node,
TupleTableSlot *slot);
static void postgresGetForeignUpperPaths(PlannerInfo *root,
@@ -476,7 +485,7 @@ static void store_returning_result(PgFdwModifyState *fmstate,
static void finish_foreign_modify(PgFdwModifyState *fmstate);
static void deallocate_query(PgFdwModifyState *fmstate);
static List *build_remote_returning(Index rtindex, Relation rel,
- List *returningList);
+ List *returningList, Var *tid);
static void rebuild_fdw_scan_tlist(ForeignScan *fscan, List *tlist);
static void execute_dml_stmt(ForeignScanState *node);
static TupleTableSlot *get_returning_data(ForeignScanState *node);
@@ -542,6 +551,11 @@ static void merge_fdw_options(PgFdwRelationInfo *fpinfo,
const PgFdwRelationInfo *fpinfo_i);
static int get_batch_size_option(Relation rel);
+static bool is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel);
+static void init_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ PgFdwRelationInfo *existing_fpinfo);
/*
* Foreign-data wrapper handler function: return a struct with pointers
@@ -597,6 +611,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
/* Support functions for join push-down */
routine->GetForeignJoinPaths = postgresGetForeignJoinPaths;
+ routine->TryShippableJoinPaths = postgresTryShippableJoinPaths;
/* Support functions for upper relation push-down */
routine->GetForeignUpperPaths = postgresGetForeignUpperPaths;
@@ -621,10 +636,32 @@ static void
postgresGetForeignRelSize(PlannerInfo *root,
RelOptInfo *baserel,
Oid foreigntableid)
+{
+ init_fpinfo(root, baserel, foreigntableid, NULL);
+}
+
+/*
+ * init_fpinfo
+ *
+ * Either initialize fpinfo based on foreign table or generate one, based on
+ * existing fpinfo.
+ * Also estimate # of rows and width of the result of the scan.
+ *
+ * We should consider the effect of all baserestrictinfo clauses here, but
+ * not any join clauses.
+ */
+static void
+init_fpinfo(PlannerInfo *root,
+ RelOptInfo *baserel,
+ Oid foreigntableid,
+ PgFdwRelationInfo *existing_fpinfo)
{
PgFdwRelationInfo *fpinfo;
ListCell *lc;
+ Assert(existing_fpinfo || foreigntableid != InvalidOid);
+ Assert(existing_fpinfo == NULL || foreigntableid == InvalidOid);
+
/*
* We use PgFdwRelationInfo to pass various information to subsequent
* functions.
@@ -635,40 +672,64 @@ postgresGetForeignRelSize(PlannerInfo *root,
/* Base foreign tables need to be pushed down always. */
fpinfo->pushdown_safe = true;
- /* Look up foreign-table catalog info. */
- fpinfo->table = GetForeignTable(foreigntableid);
- fpinfo->server = GetForeignServer(fpinfo->table->serverid);
-
- /*
- * Extract user-settable option values. Note that per-table settings of
- * use_remote_estimate, fetch_size and async_capable override per-server
- * settings of them, respectively.
- */
- fpinfo->use_remote_estimate = false;
- fpinfo->fdw_startup_cost = DEFAULT_FDW_STARTUP_COST;
- fpinfo->fdw_tuple_cost = DEFAULT_FDW_TUPLE_COST;
- fpinfo->shippable_extensions = NIL;
- fpinfo->fetch_size = 100;
- fpinfo->async_capable = false;
-
- apply_server_options(fpinfo);
- apply_table_options(fpinfo);
+ if (existing_fpinfo)
+ {
+ /* We don't have any table, related to query */
+ fpinfo->table = NULL;
+ fpinfo->server = existing_fpinfo->server;
+ }
+ else
+ {
+ /* Look up foreign-table catalog info. */
+ fpinfo->table = GetForeignTable(foreigntableid);
+ fpinfo->server = GetForeignServer(fpinfo->table->serverid);
+ }
- /*
- * If the table or the server is configured to use remote estimates,
- * identify which user to do remote access as during planning. This
- * should match what ExecCheckPermissions() does. If we fail due to lack
- * of permissions, the query would have failed at runtime anyway.
- */
- if (fpinfo->use_remote_estimate)
+ if (existing_fpinfo)
{
- Oid userid;
+ merge_fdw_options(fpinfo, existing_fpinfo, NULL);
+ fpinfo->user = existing_fpinfo->user;
- userid = OidIsValid(baserel->userid) ? baserel->userid : GetUserId();
- fpinfo->user = GetUserMapping(userid, fpinfo->server->serverid);
+ /*
+ * Don't try to execute anything on remote server for
+ * non-relation-based query
+ */
+ fpinfo->use_remote_estimate = false;
}
else
- fpinfo->user = NULL;
+ {
+ /*
+ * Extract user-settable option values. Note that per-table settings
+ * of use_remote_estimate, fetch_size and async_capable override
+ * per-server settings of them, respectively.
+ */
+ fpinfo->use_remote_estimate = false;
+ fpinfo->fdw_startup_cost = DEFAULT_FDW_STARTUP_COST;
+ fpinfo->fdw_tuple_cost = DEFAULT_FDW_TUPLE_COST;
+ fpinfo->shippable_extensions = NIL;
+ fpinfo->fetch_size = 100;
+ fpinfo->async_capable = false;
+ fpinfo->is_generated = false;
+
+ apply_server_options(fpinfo);
+ apply_table_options(fpinfo);
+
+ /*
+ * If the table or the server is configured to use remote estimates,
+ * identify which user to do remote access as during planning. This
+ * should match what ExecCheckPermissions() does. If we fail due to lack
+ * of permissions, the query would have failed at runtime anyway.
+ */
+ if (fpinfo->use_remote_estimate)
+ {
+ Oid userid;
+
+ userid = OidIsValid(baserel->userid) ? baserel->userid : GetUserId();
+ fpinfo->user = GetUserMapping(userid, fpinfo->server->serverid);
+ }
+ else
+ fpinfo->user = NULL;
+ }
/*
* Identify which baserestrictinfo clauses can be sent to the remote
@@ -783,6 +844,9 @@ postgresGetForeignRelSize(PlannerInfo *root,
fpinfo->hidden_subquery_rels = NULL;
/* Set the relation index. */
fpinfo->relation_index = baserel->relid;
+ if (existing_fpinfo)
+ /* Mark fpinfo generated */
+ fpinfo->is_generated = true;
}
/*
@@ -1476,13 +1540,72 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ RangeTblFunction *rtfunc;
+ TupleDesc td;
+ Oid funcrettype;
+ int num_funcs,
+ attnum;
+ ListCell *lc,
+ *lctype,
+ *lcname;
+ bool functype_OK = true;
+ List *functypes = NIL;
+
+ if (rte->funcordinality)
+ continue;
+
+ num_funcs = list_length(rte->functions);
+ Assert(num_funcs >= 0);
+
+ foreach(lc, rte->functions)
+ {
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, NULL);
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ {
+ functype_OK = false;
+ break;
+ }
+ functypes = lappend_oid(functypes, funcrettype);
+ }
+ if (!functype_OK)
+ continue;
+ td = CreateTemplateTupleDesc(num_funcs);
+
+ /*
+ * funcrettype != RECORD, so we have only one return attribute per
+ * function
+ */
+ Assert(list_length(rte->eref->colnames) == num_funcs);
+ attnum = 1;
+ forthree(lc, rte->functions, lctype, functypes, lcname, rte->eref->colnames)
+ {
+ char *colname;
+
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+ funcrettype = lfirst_oid(lctype);
+ colname = strVal(lfirst(lcname));
+
+ TupleDescInitEntry(td, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(td, (AttrNumber) attnum,
+ exprCollation(rtfunc->funcexpr));
+ attnum++;
+ }
+
+ assign_record_type_typmod(td);
+ att->atttypmod = td->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1518,14 +1641,26 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. In case of a join or aggregate, scan RTEs
+ * until RTE_RELATION is found. We would get the same result from any.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte && rte->rtekind == RTE_RELATION)
+ break;
+ }
+ Assert(rte && rte->rtekind == RTE_RELATION);
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -2566,8 +2701,30 @@ postgresPlanDirectModify(PlannerInfo *root,
* node below.
*/
if (fscan->scan.scanrelid == 0)
+ {
+ ListCell *lc;
+ Var *tid_var = NULL;
+
+ /*
+ * We should explicitly add tableoid to returning list if it's
+ * requested
+ */
+ foreach(lc, processed_tlist)
+ {
+ TargetEntry *tle = lfirst_node(TargetEntry, lc);
+ Var *var = (Var *) tle->expr;
+
+ if (IsA(var, Var) && (var->varattno == TableOidAttributeNumber) && (strcmp(tle->resname, "tableoid") == 0))
+ {
+ tid_var = var;
+ break;
+ }
+
+ }
+
returningList = build_remote_returning(resultRelation, rel,
- returningList);
+ returningList, tid_var);
+ }
}
/*
@@ -2889,21 +3046,66 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
- Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
- relname = get_rel_name(rte->relid);
- if (es->verbose)
+ if (rte->rtekind == RTE_RELATION)
{
- char *namespace;
-
- namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
- appendStringInfo(relations, "%s.%s",
- quote_identifier(namespace),
- quote_identifier(relname));
+ // Note: relname may be uninitialized.
+ relname = get_rel_name(rte->relid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
+ appendStringInfo(relations, "%s.%s",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfoString(relations,
+ quote_identifier(relname));
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ ListCell *lc;
+ int n;
+ bool first = true;
+
+
+ n = list_length(rte->functions);
+
+ if (n > 1)
+ appendStringInfo(relations, "ROWS FROM(");
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (!first)
+ appendStringInfoString(relations, ", ");
+ else
+ first = false;
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ {
+ FuncExpr *funcexpr = (FuncExpr *) rtfunc->funcexpr;
+ Oid funcid = funcexpr->funcid;
+
+ relname = get_func_name(funcid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name(get_func_namespace(funcid));
+ appendStringInfo(relations, "%s.%s()",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfo(relations, "%s()", quote_identifier(relname));
+ }
+ }
+ if (n > 1)
+ appendStringInfo(relations, ")");
}
- else
- appendStringInfoString(relations,
- quote_identifier(relname));
refname = (char *) list_nth(es->rtable_names, rti - 1);
if (refname == NULL)
refname = rte->eref->aliasname;
@@ -4427,7 +4629,7 @@ deallocate_query(PgFdwModifyState *fmstate)
* UPDATE/DELETE .. RETURNING on a join directly
*/
static List *
-build_remote_returning(Index rtindex, Relation rel, List *returningList)
+build_remote_returning(Index rtindex, Relation rel, List *returningList, Var *tid)
{
bool have_wholerow = false;
List *tlist = NIL;
@@ -4436,6 +4638,19 @@ build_remote_returning(Index rtindex, Relation rel, List *returningList)
Assert(returningList);
+ /*
+ * If tid is requested, add it to the returning list
+ */
+ if (tid)
+ {
+ tlist = lappend(tlist,
+ makeTargetEntry((Expr *) tid,
+ list_length(tlist) + 1,
+ NULL,
+ false));
+
+ }
+
vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
/*
@@ -4727,11 +4942,13 @@ init_returning_filter(PgFdwDirectModifyState *dmstate,
if (attrno < 0)
{
/*
- * We don't retrieve system columns other than ctid and oid.
+ * We don't retrieve system columns other than ctid and oid,
+ * but locally-generated tableoid can appear in returning
+ * list.
*/
if (attrno == SelfItemPointerAttributeNumber)
dmstate->ctidAttno = i;
- else
+ else if (attrno != TableOidAttributeNumber)
Assert(false);
dmstate->hasSystemCols = true;
}
@@ -5745,6 +5962,63 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
return commands;
}
+/*
+ * Determine if foreignrel, not backed by foreign
+ * table, is fine to push down.
+ */
+static bool
+is_nonrel_relinfo_ok(PlannerInfo *root, RelOptInfo *foreignrel)
+{
+ RangeTblEntry *rte;
+ RangeTblFunction *rtfunc;
+ TupleDesc tupdesc;
+ Oid funcrettype;
+
+ rte = planner_rt_fetch(foreignrel->relid, root);
+
+ if (!rte)
+ return false;
+
+ Assert(foreignrel->fdw_private);
+
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ ListCell *lc;
+
+ /*
+ * WITH ORDINALITY pushdown is not implemented yet.
+ */
+ if (rte->funcordinality)
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+ foreach(lc, rte->functions)
+ {
+ rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ /*
+ * Remote server requires a well defined return type for a
+ * function pushdown.
+ */
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID || funcrettype == VOIDOID)
+ return false;
+
+ if (contain_var_clause(rtfunc->funcexpr) ||
+ contain_mutable_functions(rtfunc->funcexpr) ||
+ contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, foreignrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
/*
* Check if reltarget is safe enough to push down semi-join. Reltarget is not
* safe, if it contains references to inner rel relids, which do not belong to
@@ -6477,6 +6751,43 @@ postgresGetForeignJoinPaths(PlannerInfo *root,
/* XXX Consider parameterized paths for the join relation */
}
+/*
+ * postgresTryShippableJoinPaths
+ *
+ * Try to add foreign join of foreign relation with shippable RTE.
+ */
+static void
+postgresTryShippableJoinPaths(PlannerInfo *root,
+ RelOptInfo *joinrel,
+ RelOptInfo *outerrel,
+ RelOptInfo *innerrel,
+ JoinType jointype,
+ JoinPathExtraData *extra)
+{
+ PgFdwRelationInfo *fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ PgFdwRelationInfo *fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+
+ if (fpinfo_o == NULL)
+ /* Outer path is not foreign relation or foreign JOIN. */
+ return;
+
+ if (joinrel->fdwroutine != NULL || innerrel->reloptkind != RELOPT_BASEREL)
+ return;
+
+ if (fpinfo_i == NULL || fpinfo_i->is_generated)
+ init_fpinfo(root, innerrel, InvalidOid, fpinfo_o);
+
+ if (!is_nonrel_relinfo_ok(root, innerrel))
+ return;
+
+ joinrel->serverid = outerrel->serverid;
+ joinrel->userid = outerrel->userid;
+ joinrel->useridiscurrent = outerrel->useridiscurrent;
+ joinrel->fdwroutine = outerrel->fdwroutine;
+
+ postgresGetForeignJoinPaths(root, joinrel, outerrel, innerrel, jointype, extra);
+}
+
/*
* Assess whether the aggregation, grouping and having operations can be pushed
* down to the foreign server. As a side effect, save information we obtain in
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 9e501660d1..772144d611 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -52,6 +52,12 @@ typedef struct PgFdwRelationInfo
/* True means that the query_pathkeys is safe to push down */
bool qp_is_pushdown_safe;
+ /*
+ * True means that PgFdwRelationInfo is not extracted from catalogs, but
+ * generated
+ */
+ bool is_generated;
+
/* Cost and selectivity of local_conds. */
QualCost local_conds_cost;
Selectivity local_conds_sel;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 372fe6dad1..6c717b58b8 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4278,3 +4278,151 @@ SELECT CASE WHEN closed IS NOT false THEN 1 ELSE 0 END
-- Clean up
\set VERBOSITY default
RESET debug_discard_caches;
+
+-- ===================================================================
+-- test function scan pushdown
+-- ===================================================================
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl');
+ALTER FOREIGN TABLE remote_tbl OPTIONS (use_remote_estimate 'true');
+CREATE TABLE base_tbl1 (c int, d text);
+CREATE FOREIGN TABLE remote_tbl1 (c int, d text)
+ SERVER loopback OPTIONS (table_name 'base_tbl1');
+ALTER FOREIGN TABLE remote_tbl1 OPTIONS (use_remote_estimate 'true');
+
+INSERT INTO remote_tbl SELECT g, g*2 from generate_series(1,1000) g;
+INSERT INTO remote_tbl1 SELECT g, 'text'|| g from generate_series(1,500) g;
+ANALYZE base_tbl;
+ANALYZE base_tbl1;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, unnest(array[2,3,4]) n WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
+
+SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a
+ORDER BY r.a;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*,n from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+SELECT * from remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a and n > 3;
+
+CREATE OR REPLACE FUNCTION get_constant_texts()
+RETURNS TABLE (text_value text) AS $$
+BEGIN
+ RETURN QUERY VALUES
+ ('text1'),
+ ('text4');
+END;
+$$ LANGUAGE plpgsql IMMUTABLE;
+
+ALTER EXTENSION postgres_fdw ADD FUNCTION get_constant_texts();
+ALTER SERVER loopback OPTIONS (extensions 'postgres_fdw');
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.*, t.n from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), get_constant_texts()) t (n, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+
+SELECT r.*, t.txt from remote_tbl1 r, ROWS FROM (unnest(array[3,4]), get_constant_texts()) t (n, txt)
+WHERE r.c = t.n AND r.d = t.txt;
+
+-- Complex types
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx);
+
+SELECT * FROM remote_tbl r JOIN UNNEST(array[box '((2,3),(-2,-3))']) as t(bx) ON a = area(bx)
+ORDER BY r.a;
+
+-- DML
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=5 FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM UNNEST(array[10,20], array[box '((2,3),(-2,-4))', box '((1,2),(-2,-3))']) AS t (l, bx) WHERE r.a between l and area(t.bx)
+RETURNING a,b;
+
+-- Test that local functions are not pushed down
+CREATE OR REPLACE FUNCTION f(INTEGER)
+RETURNS SETOF INTEGER
+LANGUAGE sql AS 'select generate_series(1,$1);' IMMUTABLE;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl r, f(10) n
+WHERE r.a = n;
+
+SELECT * FROM remote_tbl r, remote_tbl1 r1, unnest(array[3,4]) n
+WHERE r.a = n AND r1.c = r.a AND n > 3;
+
+-- Test that a function that returns a record is not pushed down
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$ SELECT (1,2)::record $$ language SQL IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD function f_ret_record();
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+
+DROP FUNCTION f(INTEGER);
+ALTER EXTENSION postgres_fdw DROP FUNCTION get_constant_texts();
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ret_record();
+DROP FUNCTION get_constant_texts();
+DROP FUNCTION f_ret_record();
+DROP TABLE base_tbl, base_tbl1;
+DROP FOREIGN TABLE remote_tbl, remote_tbl1;
+
+-- Test that function WITH ORDINALITY is not pushed down
+CREATE TABLE base_tbl (a int, b int);
+CREATE FOREIGN TABLE remote_tbl (a int, b int) SERVER loopback OPTIONS (table_name 'base_tbl');
+INSERT INTO remote_tbl VALUES (1, 2), (2, 3), (3, 4), (5, 6);
+ANALYZE remote_tbl;
+SET enable_mergejoin TO false;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM remote_tbl, unnest(ARRAY[1, 2]) WITH ORDINALITY
+WHERE a=unnest;
+
+SELECT * FROM remote_tbl, unnest(ARRAY[1, 2]) WITH ORDINALITY
+WHERE a=unnest;
+
+DROP TABLE base_tbl;
+DROP FOREIGN TABLE remote_tbl;
+RESET enable_mergejoin;
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 3971138480..57430a8165 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -333,6 +333,17 @@ add_paths_to_joinrel(PlannerInfo *root,
outerrel, innerrel,
jointype, &extra);
+ /*
+ * If push down of join is not possible we can try to join foreign
+ * relation with shippable RTE. In this case we have a chance to push down
+ * this join yet.
+ */
+ else if (outerrel->fdwroutine &&
+ outerrel->fdwroutine->TryShippableJoinPaths)
+ outerrel->fdwroutine->TryShippableJoinPaths(root, joinrel,
+ outerrel, innerrel,
+ jointype, &extra);
+
/*
* 6. Finally, give extensions a chance to manipulate the path list. They
* could add new paths (such as CustomPaths) by calling add_path(), or
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 91c7c4fe2f..088845c00f 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -552,7 +552,6 @@ add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos,
newrte->joinleftcols = NIL;
newrte->joinrightcols = NIL;
newrte->join_using_alias = NULL;
- newrte->functions = NIL;
newrte->tablefunc = NULL;
newrte->values_lists = NIL;
newrte->coltypes = NIL;
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index fcde3876b2..0ccd6776a8 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -221,6 +221,7 @@ typedef struct FdwRoutine
/* Functions for remote-join planning */
GetForeignJoinPaths_function GetForeignJoinPaths;
+ GetForeignJoinPaths_function TryShippableJoinPaths;
/* Functions for remote upper-relation (post scan/join) planning */
GetForeignUpperPaths_function GetForeignUpperPaths;
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f551624afb..3fda68bf7f 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -1681,10 +1681,10 @@ select pg_get_viewdef('tt14v', true);
-- ... and you can even EXPLAIN it ...
explain (verbose, costs off) select * from tt14v;
- QUERY PLAN
-----------------------------------------
+ QUERY PLAN
+--------------------------------------------
Function Scan on testviewschm2.tt14f t
- Output: t.f1, t.f3, t.f4
+ Output: t.f1, t."?dropped?column?", t.f4
Function Call: tt14f()
(3 rows)
--
2.43.0
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2021-05-20 17:43 Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2021-06-15 13:15 ` Re: Function scan FDW pushdown Ashutosh Bapat <[email protected]>
2021-10-04 07:42 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2024-11-05 16:11 ` Re: Function scan FDW pushdown [email protected]
@ 2025-08-05 21:32 ` Álvaro Herrera <[email protected]>
0 siblings, 0 replies; 14+ messages in thread
From: Álvaro Herrera @ 2025-08-05 21:32 UTC (permalink / raw)
To: PostgreSQL Hackers <[email protected]>
Hello,
On 2024-Nov-05, [email protected] wrote:
> This is a long-overdue follow-up to the original patch.
> Note that this patch contains only the changes required for
> function scan pushdown, examples and code related to asymmetric
> join are dropped.
I've marked this as returned with feedback, as several months have
passed without a further version; the current one has a number of
gotchas, including some problematic coding detected by a compiler
warning, as well as unfinished design:
> The issue with setting newrte->functions to NIL still persists.
> [...]
> I am aware that the rte->functions will now be copied even on
> instances that don't utilize a FDW, but I don't see a way to solve it.
> Any suggestions are welcome.
Feel free to reopen this CF entry[1] once you're able to figure this out.
[1] https://commitfest.postgresql.org/patch/5470/
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
@ 2026-05-18 10:34 Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Korotkov @ 2026-05-18 10:34 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi, Alexander!
The revised patch is attached.
On Tue, May 12, 2026 at 11:09 AM Alexander Pyhalov
<[email protected]> wrote:
> 1) deparseColumnRef() doesn't account for whole row vars.
> In queries like
>
> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS
> t (bx) WHERE r.a = area(t.bx)
>
> it fails with assert that varattno should be > 0. When we lock
> non-relation RTE, we select whole row var, and we have to deparse it for
> function RTE.
>
> You've removed check for function return type. This seems to be
> dangerous. Old example
>
> CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$ SELECT
> (1,2)::record $$ language SQL IMMUTABLE;
> ALTER EXTENSION postgres_fdw ADD function f_ret_record();
> EXPLAIN (VERBOSE, COSTS OFF)
> SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
> WHERE s.a = rt.a;
>
> fails with
>
> ERROR: a column definition list is required for functions returning
> "record"
function_rte_pushdown_ok() now calls get_expr_result_type() and
rejects anything that isn't TYPEFUNC_SCALAR (also RECORDOID/VOIDOID),
so f_ret_record() no longer reaches the remote side.
deparseColumnRef() now handles varattno == 0 for RTE_FUNCTION and
emits ROW(f<rti>.c1, ..., f<rti>.c<N>) from rte->eref->colnames.
> 2) postgresBeginForeignScan() can step on function RTE, and doesn't know
> what to do with it:
> SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
> ERROR: cache lookup failed for foreign table 0
>
> So, we need to look for the first RTE_RELATION, as in older patch
> version.
The scanrelid == 0 branch in postgresBeginForeignScan() now scans
fs_base_relids until it finds an RTE_RELATION. An explicit
elog(ERROR) guards the (theoretically impossible) case where no
foreign RTE is found.
> 3) A lot of complexity in the old patch version was in making it
> possible to find out RTE_FUNCTION attribute types after planing, as it's
> necessary to correctly handle joins. In this version
> get_tupdesc_for_join_scan_tuples() doesn't handle function RTEs. This
> means, when we try to find out type for attribute types for joins, we'll
> get errors. This can be seen in queries like
>
> UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM
> UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
> RETURNING a,b;
>
> Now it fails on earlier stages (with "column f2.c0 does not exist"), but
> if we fix it, we'll get something like
> "ERROR: input of anonymous composite types is not implemented"
>
> Overall, function_rte_pushdown_ok() now allows more strange
> constructions. Could it skip Vars from outside of joinrel->relids? Can
> we safely ship function with parameters in arguments? I'm not sure.
Restored the per-function metadata you had in v2/v3.
FdwScanPrivateFunctions (list of (funcid, funcrettype, funccollation)
indexed by RTI-offset) and FdwScanPrivateMinRTIndex are now saved in
fdw_private by postgresGetForeignPlan().
get_tupdesc_for_join_scan_tuples() now has an RTE_FUNCTION branch that
rebuilds the tuple descriptor from this metadata, exactly as in your
patch.
> 4) Why do we restrict list_length(rte->functions) to 1? The main reason
> for supporting several rte->functions was to allow pushdown of UNNEST()
> with several arrays, which is used by HammerDB tproc-c test.
function_rte_pushdown_ok() now loops over rte->functions and applies
the same checks to every member. deparseFunctionRangeTblRef() emits
ROWS FROM (f1(), f2(), ...) AS f<rti>(c1, c2, ...) with the
column-name list covering the concatenation of every function's
columns, in the order they appear.
> 5) We can support pushing down joins of foreign tables and another RTE
> types, for example, VALUES. But with new specific way of handling
> RTE_FUNCTIONS in core, this requires both changes to
> set_foreign_rel_properties() and postgres_fdw. Not sure, if this is a
> big problem, but at least is worth mentioning .
I've kept the planner-side change minimal:
set_foreign_rel_properties() propagates fdwroutine onto a joinrel
pairing a foreign rel with an RTE_FUNCTION rel, so the standard
GetForeignJoinPaths callback gets invoked. No new FDW callback was
needed. This is also the reason the planner-side fpinfo of the
function side lives on the joinrel
(outer_func_fpinfo/inner_func_fpinfo fields added to
PgFdwRelationInfo) rather than on the function
RelOptInfo->fdw_private: the same RTE_FUNCTION rel can be paired
independently with foreign rels from several servers. Extending the
same scheme to RTE_VALUES / RTE_CTE is, I agree, worth doing late. It
would be a one branch addition to set_foreign_rel_properties() plus an
FDW-side shippability check analogous to function_rte_pushdown_ok().
A few extra changes to the patch:
- LATERAL function RTEs are explicitly rejected in
function_rte_pushdown_ok() (returns false when rel->lateral_relids is
non-empty).
- Outer joins (LEFT/RIGHT/FULL) and SEMI joins fall through to the
existing !fpinfo_o || !fpinfo_i rejection, since
inner_is_function/outer_is_function are only set for JOIN_INNER.
- Regression coverage in contrib/postgres_fdw/sql/postgres_fdw.sql now
reuses your examples.
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v5-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch (53.3K, ../../CAPpHfduVg4-6U=oDyBLGV53nU6bVfF+3yqVo9s09TpRSjgMDWw@mail.gmail.com/2-v5-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch)
download | inline diff:
From c646befa316555ac7a2b10cf9f520f36496941cb Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Sun, 10 May 2026 17:15:21 +0300
Subject: [PATCH v5] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
column aliases.
---
contrib/postgres_fdw/deparse.c | 122 ++++-
.../postgres_fdw/expected/postgres_fdw.out | 402 ++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 431 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 10 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 162 +++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1127 insertions(+), 24 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..f40fe52ea5b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -112,6 +112,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -2030,6 +2031,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2107,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2794,44 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index aaffcf31271..ca158d5b84f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,408 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios mirror the reproducers from Pyhalov's
+-- v4-patch review (postgr.es/m/7e2abeb3ea9bc7ca024f4e457dce33f5%40postgrespro.ru),
+-- so the same dedicated foreign table is reused for each.
+CREATE TABLE base_tbl_pyh (a int, b int);
+INSERT INTO base_tbl_pyh
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_pyh');
+-- Pyhalov #3: function RTE appears first in FROM; the foreign relation
+-- is found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_pyh r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- Pyhalov #2: function returning record (composite) is forbidden;
+-- pushing it would yield a remote "column definition list is required"
+-- error. function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+-------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_pyh
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- Pyhalov #1: UPDATE ... FROM unnest() with a complex element type;
+-- area(box) yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- now emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- Pyhalov #4: same shape with CASE and RETURNING. Exercises the
+-- DirectModify path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- Pyhalov #5: ROWS FROM (...) with several functions. We force
+-- pushdown because the cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_pyh r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT ROW(f2.c1, f2.c2), r1.a FROM (public.base_tbl_pyh r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_pyh
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_pyh WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_pyh WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+---------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_pyh ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_pyh;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0ff4ec23164..1641ddd9ef9 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -740,6 +757,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -1634,9 +1654,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1657,9 +1694,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1695,13 +1738,62 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION && rtfuncdata != NIL)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ if (funcdata == NIL)
+ continue;
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1737,14 +1829,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1787,8 +1895,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2941,7 +3060,15 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: pass NIL/0 for the function
+ * metadata. We don't currently push function RTEs through the
+ * direct-modify path, so there are no whole-row Vars pointing at
+ * function-RTE tuples to reconstruct.
+ */
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, NIL, 0);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3054,7 +3181,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3104,6 +3232,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3479,8 +3623,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6619,6 +6772,181 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of three Integer nodes:
+ * (funcid, funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid = InvalidOid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6634,6 +6962,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6652,15 +6982,70 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..5b2ffcf06f7 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 267d3c1a7e7..fbb0c3f2785 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,168 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios mirror the reproducers from Pyhalov's
+-- v4-patch review (postgr.es/m/7e2abeb3ea9bc7ca024f4e457dce33f5%40postgrespro.ru),
+-- so the same dedicated foreign table is reused for each.
+CREATE TABLE base_tbl_pyh (a int, b int);
+INSERT INTO base_tbl_pyh
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_pyh');
+
+-- Pyhalov #3: function RTE appears first in FROM; the foreign relation
+-- is found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- Pyhalov #2: function returning record (composite) is forbidden;
+-- pushing it would yield a remote "column definition list is required"
+-- error. function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- Pyhalov #1: UPDATE ... FROM unnest() with a complex element type;
+-- area(box) yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- now emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- Pyhalov #4: same shape with CASE and RETURNING. Exercises the
+-- DirectModify path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- Pyhalov #5: ROWS FROM (...) with several functions. We force
+-- pushdown because the cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_pyh;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
@ 2026-05-18 20:06 ` Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Pyhalov @ 2026-05-18 20:06 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Alexander Korotkov писал(а) 2026-05-18 13:34:
> Hi, Alexander!
>
> The revised patch is attached.
>
> On Tue, May 12, 2026 at 11:09 AM Alexander Pyhalov
> <[email protected]> wrote:
>> 1) deparseColumnRef() doesn't account for whole row vars.
>> In queries like
>>
>> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))'])
>> AS
>> t (bx) WHERE r.a = area(t.bx)
>>
>> it fails with assert that varattno should be > 0. When we lock
>> non-relation RTE, we select whole row var, and we have to deparse it
>> for
>> function RTE.
>>
>> You've removed check for function return type. This seems to be
>> dangerous. Old example
>>
>> CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$ SELECT
>> (1,2)::record $$ language SQL IMMUTABLE;
>> ALTER EXTENSION postgres_fdw ADD function f_ret_record();
>> EXPLAIN (VERBOSE, COSTS OFF)
>> SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
>> WHERE s.a = rt.a;
>>
>> fails with
>>
>> ERROR: a column definition list is required for functions returning
>> "record"
>
> function_rte_pushdown_ok() now calls get_expr_result_type() and
> rejects anything that isn't TYPEFUNC_SCALAR (also RECORDOID/VOIDOID),
> so f_ret_record() no longer reaches the remote side.
> deparseColumnRef() now handles varattno == 0 for RTE_FUNCTION and
> emits ROW(f<rti>.c1, ..., f<rti>.c<N>) from rte->eref->colnames.
>
>> 2) postgresBeginForeignScan() can step on function RTE, and doesn't
>> know
>> what to do with it:
>> SELECT * FROM unnest(array[2,3,4]) n, remote_tbl r WHERE r.a = n;
>> ERROR: cache lookup failed for foreign table 0
>>
>> So, we need to look for the first RTE_RELATION, as in older patch
>> version.
>
> The scanrelid == 0 branch in postgresBeginForeignScan() now scans
> fs_base_relids until it finds an RTE_RELATION. An explicit
> elog(ERROR) guards the (theoretically impossible) case where no
> foreign RTE is found.
>
>> 3) A lot of complexity in the old patch version was in making it
>> possible to find out RTE_FUNCTION attribute types after planing, as
>> it's
>> necessary to correctly handle joins. In this version
>> get_tupdesc_for_join_scan_tuples() doesn't handle function RTEs. This
>> means, when we try to find out type for attribute types for joins,
>> we'll
>> get errors. This can be seen in queries like
>>
>> UPDATE remote_tbl r SET b=CASE WHEN random()>=0 THEN 5 ELSE 0 END FROM
>> UNNEST(array[box '((2,3),(-2,-3))']) AS t (bx) WHERE r.a = area(t.bx)
>> RETURNING a,b;
>>
>> Now it fails on earlier stages (with "column f2.c0 does not exist"),
>> but
>> if we fix it, we'll get something like
>> "ERROR: input of anonymous composite types is not implemented"
>>
>> Overall, function_rte_pushdown_ok() now allows more strange
>> constructions. Could it skip Vars from outside of joinrel->relids? Can
>> we safely ship function with parameters in arguments? I'm not sure.
>
> Restored the per-function metadata you had in v2/v3.
> FdwScanPrivateFunctions (list of (funcid, funcrettype, funccollation)
> indexed by RTI-offset) and FdwScanPrivateMinRTIndex are now saved in
> fdw_private by postgresGetForeignPlan().
> get_tupdesc_for_join_scan_tuples() now has an RTE_FUNCTION branch that
> rebuilds the tuple descriptor from this metadata, exactly as in your
> patch.
Hi. I am a bit confused about this comment (and code):
/*
* DirectModify on a foreign join: pass NIL/0 for
the function
* metadata. We don't currently push function
RTEs through the
* direct-modify path, so there are no whole-row
Vars pointing at
* function-RTE tuples to reconstruct.
*/
tupdesc = get_tupdesc_for_join_scan_tuples(node,
NIL, 0);
We evidently go through this code path when executing example
UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS
t (bx) WHERE r.a = area(t.bx)
RETURNING a,b;
But don't need whole row var in returning list.... However, we still can
step on this issue.
UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))'],
array[int '1']) AS t (bx, i) WHERE r.a = area(t.bx)
RETURNING a,b,t;
ERROR: input of anonymous composite types is not implemented
CONTEXT: whole-row reference to foreign table "t"
--
Best regards,
Alexander Pyhalov,
Postgres Professional
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
@ 2026-05-19 13:00 ` Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Korotkov @ 2026-05-19 13:00 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi, Alexander.
On Mon, May 18, 2026 at 11:06 PM Alexander Pyhalov
<[email protected]> wrote:
> Hi. I am a bit confused about this comment (and code):
>
> /*
> * DirectModify on a foreign join: pass NIL/0 for
> the function
> * metadata. We don't currently push function
> RTEs through the
> * direct-modify path, so there are no whole-row
> Vars pointing at
> * function-RTE tuples to reconstruct.
> */
> tupdesc = get_tupdesc_for_join_scan_tuples(node,
> NIL, 0);
>
> We evidently go through this code path when executing example
>
> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))']) AS
> t (bx) WHERE r.a = area(t.bx)
> RETURNING a,b;
>
> But don't need whole row var in returning list.... However, we still can
> step on this issue.
Yes, we go through this code path, and it works as long as whole-row
var is not needed.
> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))'],
> array[int '1']) AS t (bx, i) WHERE r.a = area(t.bx)
> RETURNING a,b,t;
>
> ERROR: input of anonymous composite types is not implemented
> CONTEXT: whole-row reference to foreign table "t"
But if whole row var is actually used, then the assumption is broken.
So, we need to build a whole-row var anyway. I've fixed this in the
attached patch, and added your sample query as a regression test case.
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v6-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch (56.1K, ../../CAPpHfdvkPk0dLFbF-2DG7hV5zKNPcdyV4Xzj99chYiqAFtvh_Q@mail.gmail.com/2-v6-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch)
download | inline diff:
From 039989553ee5dacb944839ffc7a92a19c6610f5c Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Sun, 10 May 2026 17:15:21 +0300
Subject: [PATCH v6] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
column aliases.
---
contrib/postgres_fdw/deparse.c | 122 ++++-
.../postgres_fdw/expected/postgres_fdw.out | 417 ++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 455 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 10 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 173 +++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1177 insertions(+), 24 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..f40fe52ea5b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -112,6 +112,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -2030,6 +2031,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2107,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2794,44 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..342eefbb9cc 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,423 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT ROW(f2.c1, f2.c2), r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0a589f8db74..9cb6a1772d9 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -741,6 +767,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -1635,9 +1664,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1658,9 +1704,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1696,13 +1748,62 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION && rtfuncdata != NIL)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ if (funcdata == NIL)
+ continue;
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1738,14 +1839,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1788,8 +1905,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2829,6 +2957,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2942,7 +3074,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3055,7 +3206,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3105,6 +3257,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3480,8 +3648,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6639,6 +6816,181 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of three Integer nodes:
+ * (funcid, funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid = InvalidOid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6654,6 +7006,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6672,15 +7026,70 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..5b2ffcf06f7 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..95b4d7646fa 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,179 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
@ 2026-05-19 15:25 ` Alexander Pyhalov <[email protected]>
2026-05-19 18:21 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Pyhalov @ 2026-05-19 15:25 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Alexander Korotkov писал(а) 2026-05-19 16:00:
> Hi, Alexander.
>
> On Mon, May 18, 2026 at 11:06 PM Alexander Pyhalov
> <[email protected]> wrote:
>> Hi. I am a bit confused about this comment (and code):
>>
>> /*
>> * DirectModify on a foreign join: pass NIL/0
>> for
>> the function
>> * metadata. We don't currently push function
>> RTEs through the
>> * direct-modify path, so there are no
>> whole-row
>> Vars pointing at
>> * function-RTE tuples to reconstruct.
>> */
>> tupdesc =
>> get_tupdesc_for_join_scan_tuples(node,
>> NIL, 0);
>>
>> We evidently go through this code path when executing example
>>
>> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))'])
>> AS
>> t (bx) WHERE r.a = area(t.bx)
>> RETURNING a,b;
>>
>> But don't need whole row var in returning list.... However, we still
>> can
>> step on this issue.
>
> Yes, we go through this code path, and it works as long as whole-row
> var is not needed.
>
>> UPDATE remote_tbl r SET b=5 FROM UNNEST(array[box '((2,3),(-2,-3))'],
>> array[int '1']) AS t (bx, i) WHERE r.a = area(t.bx)
>> RETURNING a,b,t;
>>
>> ERROR: input of anonymous composite types is not implemented
>> CONTEXT: whole-row reference to foreign table "t"
>
> But if whole row var is actually used, then the assumption is broken.
> So, we need to build a whole-row var anyway. I've fixed this in the
> attached patch, and added your sample query as a regression test case.
>
Good evening.
Found one more issue in whole row var deparsing. It can appear on a
nullable outer side, and we should use the same logic as when deparsing
table column reference. Otherwise we get records from nulls instead of
nulls (for example, "(NULL, NULL)" instead of NULL).
Also I wonder if it is possible for get_tupdesc_for_join_scan_tuples()
to get NULL rtfuncdata when it looks at RTE_FUNCTION RTE here:
1759 else if (rte->rtekind == RTE_FUNCTION && rtfuncdata
!= NIL)
1760 {
1761 /*
1762 * A whole-row Var points at a FUNCTION RTE
absorbed into the
1763 * foreign join. Synthesize an anonymous
composite TupleDesc from
1764 * the per-function return-type metadata we
saved at plan time;
1765 * the deparser emits these as
ROW(f<rti>.c1, f<rti>.c2, ...).
1766 */
1767 List *funcdata;
1768 TupleDesc rte_tupdesc;
1769 int num_funcs;
1770 int attnum;
1771 ListCell *lc1,
?
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v7-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch (58.7K, ../../[email protected]/2-v7-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch)
download | inline diff:
From 2b78decae1c0d333ca04bd1394012f43cb14aea3 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Tue, 19 May 2026 16:28:22 +0300
Subject: [PATCH] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
column aliases.
---
contrib/postgres_fdw/deparse.c | 132 ++++-
.../postgres_fdw/expected/postgres_fdw.out | 441 +++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 455 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 10 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 183 +++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1221 insertions(+), 24 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..5567fdb8ed9 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -112,6 +112,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -2030,6 +2031,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2107,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2794,54 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ if (qualify_col)
+ {
+ appendStringInfoString(buf, "CASE WHEN (");
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfoString(buf, "*)::text IS NOT NULL THEN ");
+ }
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+
+ if (qualify_col)
+ appendStringInfoString(buf, " END");
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index aaffcf31271..c21d0876d39 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,447 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT CASE WHEN (f2.*)::text IS NOT NULL THEN ROW(f2.c1, f2.c2) END, r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: u.*, ft5.c1, ft5.c2, ft4.c1, ft4.c2
+ Relations: (public.ft5) LEFT JOIN ((public.ft4) INNER JOIN (Function u))
+ Remote SQL: SELECT CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END, r1.c1, r1.c2, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 LEFT JOIN ("S 1"."T 3" r2 INNER JOIN ROWS FROM (unnest('{1,2}'::integer[]), unnest('{3,4}'::integer[])) f3(c1, c2) ON (((r2.c1 = f3.c1)))) ON (((r1.c1 = r2.c1)))) WHERE ((r1.c1 < 10)) ORDER BY r1.c1 ASC NULLS LAST
+(4 rows)
+
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ u | c1 | c2 | c1 | c2
+---+----+----+----+----
+ | 3 | 4 | |
+ | 6 | 7 | |
+ | 9 | 10 | |
+(3 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0ff4ec23164..dc552a847d8 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -740,6 +766,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -1634,9 +1663,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1657,9 +1703,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1695,13 +1747,62 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION && rtfuncdata != NIL)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ if (funcdata == NIL)
+ continue;
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1737,14 +1838,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1787,8 +1904,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2828,6 +2956,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2941,7 +3073,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3054,7 +3205,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3104,6 +3256,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3479,8 +3647,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6619,6 +6796,181 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of three Integer nodes:
+ * (funcid, funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid = InvalidOid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6634,6 +6986,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6652,15 +7006,70 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..5b2ffcf06f7 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 267d3c1a7e7..f49fbc9317e 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,189 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.43.0
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
@ 2026-05-19 18:21 ` Alexander Korotkov <[email protected]>
2026-05-20 10:17 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Korotkov @ 2026-05-19 18:21 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Good evening!
On Tue, May 19, 2026 at 6:25 PM Alexander Pyhalov
<[email protected]> wrote:
>
> Found one more issue in whole row var deparsing. It can appear on a
> nullable outer side, and we should use the same logic as when deparsing
> table column reference. Otherwise we get records from nulls instead of
> nulls (for example, "(NULL, NULL)" instead of NULL).
Good catch, accepted.
> Also I wonder if it is possible for get_tupdesc_for_join_scan_tuples()
> to get NULL rtfuncdata when it looks at RTE_FUNCTION RTE here:
>
> 1759 else if (rte->rtekind == RTE_FUNCTION && rtfuncdata
> != NIL)
> 1760 {
> 1761 /*
> 1762 * A whole-row Var points at a FUNCTION RTE
> absorbed into the
> 1763 * foreign join. Synthesize an anonymous
> composite TupleDesc from
> 1764 * the per-function return-type metadata we
> saved at plan time;
> 1765 * the deparser emits these as
> ROW(f<rti>.c1, f<rti>.c2, ...).
> 1766 */
> 1767 List *funcdata;
> 1768 TupleDesc rte_tupdesc;
> 1769 int num_funcs;
> 1770 int attnum;
> 1771 ListCell *lc1,
>
> ?
I don't see how that's possible. I think it would be safer to just
assert rtfuncdata/funcdata are not NULL. Revised patch is attached.
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v8-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch (59.0K, ../../CAPpHfduLKwM=Jj-0q+vWiwNOLYfhJQFs+c-YPF8=voPD1M_trQ@mail.gmail.com/2-v8-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-.patch)
download | inline diff:
From fa6e090e600acf527dc2e8877c4e61c698258409 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Tue, 19 May 2026 16:28:22 +0300
Subject: [PATCH v8] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
column aliases.
---
contrib/postgres_fdw/deparse.c | 132 ++++-
.../postgres_fdw/expected/postgres_fdw.out | 441 +++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 461 +++++++++++++++++-
contrib/postgres_fdw/postgres_fdw.h | 10 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 183 +++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1227 insertions(+), 24 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..5567fdb8ed9 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -112,6 +112,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -2030,6 +2031,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2107,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2794,54 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ if (qualify_col)
+ {
+ appendStringInfoString(buf, "CASE WHEN (");
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfoString(buf, "*)::text IS NOT NULL THEN ");
+ }
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+
+ if (qualify_col)
+ appendStringInfoString(buf, " END");
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index e90289e4ab1..a626b5e5ace 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,447 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT CASE WHEN (f2.*)::text IS NOT NULL THEN ROW(f2.c1, f2.c2) END, r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: u.*, ft5.c1, ft5.c2, ft4.c1, ft4.c2
+ Relations: (public.ft5) LEFT JOIN ((public.ft4) INNER JOIN (Function u))
+ Remote SQL: SELECT CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END, r1.c1, r1.c2, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 LEFT JOIN ("S 1"."T 3" r2 INNER JOIN ROWS FROM (unnest('{1,2}'::integer[]), unnest('{3,4}'::integer[])) f3(c1, c2) ON (((r2.c1 = f3.c1)))) ON (((r1.c1 = r2.c1)))) WHERE ((r1.c1 < 10)) ORDER BY r1.c1 ASC NULLS LAST
+(4 rows)
+
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ u | c1 | c2 | c1 | c2
+---+----+----+----+----
+ | 3 | 4 | |
+ | 6 | 7 | |
+ | 9 | 10 | |
+(3 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0a589f8db74..90d24ac4d56 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -741,6 +767,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -1635,9 +1664,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1658,9 +1704,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1696,13 +1748,68 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ *
+ * For an upperrel scan (the only path that reaches here)
+ * postgresGetForeignPlan always builds rtfuncdata via
+ * get_functions_data(), so it is never NIL; the per-RTE slot for
+ * the function RTE referenced by this Var must likewise be
+ * populated.
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ Assert(rtfuncdata != NIL);
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ Assert(funcdata != NIL);
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1738,14 +1845,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1788,8 +1911,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2829,6 +2963,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2942,7 +3080,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3055,7 +3212,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3105,6 +3263,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3480,8 +3654,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6639,6 +6822,181 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of three Integer nodes:
+ * (funcid, funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid = InvalidOid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6654,6 +7012,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6672,15 +7032,70 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..5b2ffcf06f7 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index dfc58beb0d2..ec791e2127d 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,189 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 18:21 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
@ 2026-05-20 10:17 ` Alexander Pyhalov <[email protected]>
2026-07-06 14:11 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Pyhalov @ 2026-05-20 10:17 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Alexander Korotkov писал(а) 2026-05-19 21:21:
> Good evening!
>
> On Tue, May 19, 2026 at 6:25 PM Alexander Pyhalov
> <[email protected]> wrote:
>>
>> Found one more issue in whole row var deparsing. It can appear on a
>> nullable outer side, and we should use the same logic as when
>> deparsing
>> table column reference. Otherwise we get records from nulls instead of
>> nulls (for example, "(NULL, NULL)" instead of NULL).
>
>
> Good catch, accepted.
>
Hi.
I've found another issue. The fact that in the new versions of the patch
RTE RelOptInfo misses fdw_private seems to be unfortunate. For example,
in the last version we haven't thought about classifying
baserestrictinfo. And if we do, we should pass fdw_private down to
foreign_expr_walker. Perhaps, we could attach it to RTE_FUNCTION rel
prior to calling classifyConditions(), but should we later set it back
to NULL? Another problem comes if we try to handle joins, which can
crearte subqueries (like INNER/OUTER UNIQUE). In this case we should
somehow cook fpinfo for get_relation_column_alias_ids(). Attaching patch
which tries to handle baserestrictinfos by passing fpinfo down to
foreign_expr_walker().
One more interesting example (included in the patch) is
EXPLAIN (VERBOSE, COSTS OFF)
WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
JOIN LATERAL
(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
ON true)
SELECT * FROM s ORDER BY 1;
We get the following plan:
Sort
Output: s.a, s.b
Sort Key: s.a
CTE s
-> Nested Loop
Output: r1.a, r1.b
-> Foreign Scan on public.remote_tbl r1
Output: r1.a, r1.b
Remote SQL: SELECT a, b FROM public.base_tbl_fn
-> Foreign Scan
Output: NULL::integer
Relations: (public.remote_tbl r2) INNER JOIN (Function
f)
Remote SQL: SELECT NULL FROM (public.base_tbl_fn r1
INNER JOIN public.f($1::integer) f2(c1) ON (TRUE)) LIMIT 1::bigint
-> CTE Scan on s
Output: s.a, s.b
Here you can see that we use parameter in function argument. Don't know
if it's a real problem, but at least looks suspicious. In v3 patch used
contain_param_walker() in is_nonrel_relinfo_ok() (which mutated to
function_rte_pushdown_ok()) to avoid such plans.
One minor issue I've noticed is in function_rte_pushdown_ok():
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
Is the second rtekind check necessary?
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v9-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-joi.patch (72.4K, ../../[email protected]/2-v9-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign-joi.patch)
download | inline diff:
From 80684442324aa12a6004da40f09e34b573130cb2 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 20 May 2026 09:22:51 +0300
Subject: [PATCH] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
---
contrib/postgres_fdw/deparse.c | 143 ++++-
.../postgres_fdw/expected/postgres_fdw.out | 560 ++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 499 ++++++++++++++--
contrib/postgres_fdw/postgres_fdw.h | 12 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 222 +++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1416 insertions(+), 44 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..a2e9301fb65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -68,6 +68,7 @@ typedef struct foreign_glob_cxt
{
PlannerInfo *root; /* global planner state */
RelOptInfo *foreignrel; /* the foreign relation we are planning for */
+ PgFdwRelationInfo *fpinfo; /* the foreign server info we should rely on */
Relids relids; /* relids of base relations in the underlying
* scan */
} foreign_glob_cxt;
@@ -112,6 +113,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -217,6 +219,7 @@ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
void
classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds)
@@ -230,7 +233,7 @@ classifyConditions(PlannerInfo *root,
{
RestrictInfo *ri = lfirst_node(RestrictInfo, lc);
- if (is_foreign_expr(root, baserel, ri->clause))
+ if (is_foreign_expr(root, baserel, fpinfo, ri->clause))
*remote_conds = lappend(*remote_conds, ri);
else
*local_conds = lappend(*local_conds, ri);
@@ -243,11 +246,11 @@ classifyConditions(PlannerInfo *root,
bool
is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr)
{
foreign_glob_cxt glob_cxt;
foreign_loc_cxt loc_cxt;
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);
/*
* Check that the expression consists of nodes that are safe to execute
@@ -255,6 +258,7 @@ is_foreign_expr(PlannerInfo *root,
*/
glob_cxt.root = root;
glob_cxt.foreignrel = baserel;
+ glob_cxt.fpinfo = fpinfo;
/*
* For an upper relation, use relids from its underneath scan relation,
@@ -324,8 +328,8 @@ foreign_expr_walker(Node *node,
if (node == NULL)
return true;
- /* May need server info from baserel's fdw_private struct */
- fpinfo = (PgFdwRelationInfo *) (glob_cxt->foreignrel->fdw_private);
+ /* May need server info from global context */
+ fpinfo = glob_cxt->fpinfo;
/* Set up inner_cxt for possible recursion to child nodes */
inner_cxt.collation = InvalidOid;
@@ -2030,6 +2034,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2110,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2797,54 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ if (qualify_col)
+ {
+ appendStringInfoString(buf, "CASE WHEN (");
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfoString(buf, "*)::text IS NOT NULL THEN ");
+ }
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+
+ if (qualify_col)
+ appendStringInfoString(buf, " END");
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index aaffcf31271..04a47dc03ed 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,566 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a)) AND ((f1.c1 > 3))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+ QUERY PLAN
+-----------------------------------------------------------
+ Hash Join
+ Output: n.n, r.a, r.b
+ Hash Cond: (r.a = n.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4}'::integer[])
+ Filter: (n.n > f_local(3))
+(12 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+DROP FUNCTION f_local(int);
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT CASE WHEN (f2.*)::text IS NOT NULL THEN ROW(f2.c1, f2.c2) END, r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: u.*, ft5.c1, ft5.c2, ft4.c1, ft4.c2
+ Relations: (public.ft5) LEFT JOIN ((public.ft4) INNER JOIN (Function u))
+ Remote SQL: SELECT CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END, r1.c1, r1.c2, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 LEFT JOIN ("S 1"."T 3" r2 INNER JOIN ROWS FROM (unnest('{1,2}'::integer[]), unnest('{3,4}'::integer[])) f3(c1, c2) ON (((r2.c1 = f3.c1)))) ON (((r1.c1 = r2.c1)))) WHERE ((r1.c1 < 10)) ORDER BY r1.c1 ASC NULLS LAST
+(4 rows)
+
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ u | c1 | c2 | c1 | c2
+---+----+----+----+----
+ | 3 | 4 | |
+ | 6 | 7 | |
+ | 9 | 10 | |
+(3 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: s.a, s.b
+ Sort Key: s.a
+ CTE s
+ -> Nested Loop
+ Output: r1.a, r1.b
+ -> Foreign Scan on public.remote_tbl r1
+ Output: r1.a, r1.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Foreign Scan
+ Output: NULL::integer
+ Relations: (public.remote_tbl r2) INNER JOIN (Function f)
+ Remote SQL: SELECT NULL FROM (public.base_tbl_fn r1 INNER JOIN public.f($1::integer) f2(c1) ON (TRUE)) LIMIT 1::bigint
+ -> CTE Scan on s
+ Output: s.a, s.b
+(15 rows)
+
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ a | b
+----+-----
+ 1 | 101
+ 2 | 102
+ 3 | 103
+ 4 | 104
+ 5 | 105
+ 6 | 106
+ 7 | 107
+ 8 | 108
+ 9 | 109
+ 10 | 110
+ 11 | 111
+ 12 | 112
+ 13 | 113
+ 14 | 114
+ 15 | 115
+ 16 | 116
+ 17 | 117
+ 18 | 118
+ 19 | 119
+ 20 | 120
+ 21 | 121
+ 22 | 122
+ 23 | 123
+ 24 | 7
+ 25 | 125
+ 26 | 126
+ 27 | 127
+ 28 | 128
+ 29 | 129
+ 30 | 130
+(30 rows)
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0ff4ec23164..cd7046db46d 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -740,6 +766,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -893,7 +922,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* Identify which baserestrictinfo clauses can be sent to the remote
* server and which can't.
*/
- classifyConditions(root, baserel, baserel->baserestrictinfo,
+ classifyConditions(root, baserel, fpinfo, baserel->baserestrictinfo,
&fpinfo->remote_conds, &fpinfo->local_conds);
/*
@@ -1297,7 +1326,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1373,7 +1402,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1513,7 +1542,7 @@ postgresGetForeignPlan(PlannerInfo *root,
remote_exprs = lappend(remote_exprs, rinfo->clause);
else if (list_member_ptr(fpinfo->local_conds, rinfo))
local_exprs = lappend(local_exprs, rinfo->clause);
- else if (is_foreign_expr(root, foreignrel, rinfo->clause))
+ else if (is_foreign_expr(root, foreignrel, fpinfo, rinfo->clause))
remote_exprs = lappend(remote_exprs, rinfo->clause);
else
local_exprs = lappend(local_exprs, rinfo->clause);
@@ -1634,9 +1663,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1657,9 +1703,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1695,13 +1747,68 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ *
+ * For an upperrel scan (the only path that reaches here)
+ * postgresGetForeignPlan always builds rtfuncdata via
+ * get_functions_data(), so it is never NIL; the per-RTE slot for
+ * the function RTE referenced by this Var must likewise be
+ * populated.
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ Assert(rtfuncdata != NIL);
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ Assert(funcdata != NIL);
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1737,14 +1844,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1787,8 +1910,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2741,7 +2875,7 @@ postgresPlanDirectModify(PlannerInfo *root,
if (attno <= InvalidAttrNumber) /* shouldn't happen */
elog(ERROR, "system-column update is not supported");
- if (!is_foreign_expr(root, foreignrel, (Expr *) tle->expr))
+ if (!is_foreign_expr(root, foreignrel, fpinfo, (Expr *) tle->expr))
return false;
}
}
@@ -2828,6 +2962,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2941,7 +3079,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3054,7 +3211,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3104,6 +3262,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3346,7 +3520,7 @@ estimate_path_cost_size(PlannerInfo *root,
* param_join_conds might contain both clauses that are safe to send
* across, and clauses that aren't.
*/
- classifyConditions(root, foreignrel, param_join_conds,
+ classifyConditions(root, foreignrel, fpinfo, param_join_conds,
&remote_param_join_conds, &local_param_join_conds);
/* Build the list of columns to be fetched from the foreign server. */
@@ -3479,8 +3653,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6619,6 +6802,181 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of three Integer nodes:
+ * (funcid, funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid = InvalidOid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ if (!OidIsValid(funcrettype) || funcrettype == RECORDOID)
+ elog(ERROR,
+ "could not determine return type for function in foreign scan");
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ if (IsA(rtfunc->funcexpr, FuncExpr))
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+ if (rte->rtekind != RTE_FUNCTION)
+ return false;
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, fdwrel->fdw_private, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6634,6 +6992,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6652,15 +7012,76 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+
+ classifyConditions(root, innerrel, fpinfo, innerrel->baserestrictinfo,
+ &fpinfo_i->remote_conds, &fpinfo_i->local_conds);
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+
+ classifyConditions(root, outerrel, fpinfo, outerrel->baserestrictinfo,
+ &fpinfo_o->remote_conds, &fpinfo_o->local_conds);
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
@@ -6698,7 +7119,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
foreach(lc, extra->restrictlist)
{
RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
- bool is_remote_clause = is_foreign_expr(root, joinrel,
+ bool is_remote_clause = is_foreign_expr(root, joinrel, fpinfo,
rinfo->clause);
if (IS_OUTER_JOIN(jointype) &&
@@ -7396,7 +7817,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* If any GROUP BY expression is not shippable, then we cannot
* push down aggregation to the foreign server.
*/
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
/*
@@ -7424,7 +7845,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* Non-grouping expression we need to compute. Can we ship it
* as-is to the foreign server?
*/
- if (is_foreign_expr(root, grouped_rel, expr) &&
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr) &&
!is_foreign_param(root, grouped_rel, expr))
{
/* Yes, so add to tlist as-is; OK to suppress duplicates */
@@ -7444,7 +7865,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* don't have to check is_foreign_param, since that certainly
* won't return true for any such expression.)
*/
- if (!is_foreign_expr(root, grouped_rel, (Expr *) aggvars))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, (Expr *) aggvars))
return false;
/*
@@ -7496,7 +7917,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
grouped_rel->relids,
NULL,
NULL);
- if (is_foreign_expr(root, grouped_rel, expr))
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr))
fpinfo->remote_conds = lappend(fpinfo->remote_conds, rinfo);
else
fpinfo->local_conds = lappend(fpinfo->local_conds, rinfo);
@@ -7533,7 +7954,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
*/
if (IsA(expr, Aggref))
{
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
tlist = add_to_flat_tlist(tlist, list_make1(expr));
@@ -8046,8 +8467,8 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel,
* Also, the LIMIT/OFFSET cannot be pushed down, if their expressions are
* not safe to remote.
*/
- if (!is_foreign_expr(root, input_rel, (Expr *) parse->limitOffset) ||
- !is_foreign_expr(root, input_rel, (Expr *) parse->limitCount))
+ if (!is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitOffset) ||
+ !is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitCount))
return;
/* Safe to push down */
@@ -8693,7 +9114,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
- is_foreign_expr(root, rel, em->em_expr))
+ is_foreign_expr(root, rel, fpinfo, em->em_expr))
return em;
}
@@ -8764,7 +9185,7 @@ find_em_for_rel_target(PlannerInfo *root, EquivalenceClass *ec,
continue;
/* Check that expression (including relabels!) is shippable */
- if (is_foreign_expr(root, rel, em->em_expr))
+ if (is_foreign_expr(root, rel, rel->fdw_private, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..da7da1c2ea9 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
@@ -183,11 +193,13 @@ extern char *pgfdw_application_name;
/* in deparse.c */
extern void classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds);
extern bool is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr);
extern bool is_foreign_param(PlannerInfo *root,
RelOptInfo *baserel,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 267d3c1a7e7..7988bec4df7 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,228 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+
+DROP FUNCTION f_local(int);
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.43.0
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 18:21 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-20 10:17 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
@ 2026-07-06 14:11 ` Alexander Korotkov <[email protected]>
2026-07-07 10:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
0 siblings, 1 reply; 14+ messages in thread
From: Alexander Korotkov @ 2026-07-06 14:11 UTC (permalink / raw)
To: Alexander Pyhalov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Hi, Alexander!
On Wed, May 20, 2026 at 1:17 PM Alexander Pyhalov
<[email protected]> wrote:
> I've found another issue. The fact that in the new versions of the patch
> RTE RelOptInfo misses fdw_private seems to be unfortunate. For example,
> in the last version we haven't thought about classifying
> baserestrictinfo. And if we do, we should pass fdw_private down to
> foreign_expr_walker. Perhaps, we could attach it to RTE_FUNCTION rel
> prior to calling classifyConditions(), but should we later set it back
> to NULL? Another problem comes if we try to handle joins, which can
> crearte subqueries (like INNER/OUTER UNIQUE). In this case we should
> somehow cook fpinfo for get_relation_column_alias_ids(). Attaching patch
> which tries to handle baserestrictinfos by passing fpinfo down to
> foreign_expr_walker().
I think passing fpinfo down to foreign_expr_walker() is the right
approach, thank you. Attaching it to RTE_FUNCTION rel, and reseting
it back to NULL would create excessive state and potential source of
bugs. The only advantage is saving existing function signatures, but
it doesn't worth it.
Also, I don't think we need to care bout
get_relation_column_alias_ids(). As we currently pushdown function
only for inner joins, make_outerrel_subquery/make_innerrel_subquery
are always false, lower_subquery_rels also wouldn't contain
RTE_FUNCTION relid. Therefore:
* In deparseFromExprForRel() for functional rel deparseRangeTblRef()
is always called with make_subquery=false.
* is_subquery_var() returns false.
> One more interesting example (included in the patch) is
>
> EXPLAIN (VERBOSE, COSTS OFF)
> WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
> JOIN LATERAL
> (SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
> ON true)
> SELECT * FROM s ORDER BY 1;
>
> We get the following plan:
>
> Sort
> Output: s.a, s.b
> Sort Key: s.a
> CTE s
> -> Nested Loop
> Output: r1.a, r1.b
> -> Foreign Scan on public.remote_tbl r1
> Output: r1.a, r1.b
> Remote SQL: SELECT a, b FROM public.base_tbl_fn
> -> Foreign Scan
> Output: NULL::integer
> Relations: (public.remote_tbl r2) INNER JOIN (Function
> f)
> Remote SQL: SELECT NULL FROM (public.base_tbl_fn r1
> INNER JOIN public.f($1::integer) f2(c1) ON (TRUE)) LIMIT 1::bigint
> -> CTE Scan on s
> Output: s.a, s.b
>
> Here you can see that we use parameter in function argument. Don't know
> if it's a real problem, but at least looks suspicious. In v3 patch used
> contain_param_walker() in is_nonrel_relinfo_ok() (which mutated to
> function_rte_pushdown_ok()) to avoid such plans.
AFAICS, there is no correctness problem.
* The bms_is_empty(rel->lateral_relids) check rejects a lateral
reference to a relation at the same query level.
* In the example, f(r1.a) references r1 from an *outer* level. When
the subquery is planned, r1.a becomes a PARAM_EXEC, and the function
rel's lateral_relids inside the subquery is empty -- so the check
passes and funcexpr retains a Param.
* foreign_expr_walker deliberately allows PARAM_EXEC
(deparse.c:486–505), and postgres_fdw's normal machinery (params_list
=> fdw_exprs) re-sends the param on each rescan.
So there's no need to bring back v3's contain_param_walker() — this is
actually a useful capability (a parameterized foreign scan). I only
add a comment in function_rte_pushdown_ok() noting that outer-level
Params are allowed intentionally and handled as an ordinary
parameterized foreign scan.
> One minor issue I've noticed is in function_rte_pushdown_ok():
> + if (rel->rtekind != RTE_FUNCTION)
> + return false;
> + rte = planner_rt_fetch(rel->relid, root);
> + if (rte->rtekind != RTE_FUNCTION)
> + return false;
>
> Is the second rtekind check necessary?
The second check is not necessary, changed to an assert.
Other changes I made:
1. In foreign_join_ok(), the function side's baserestrictinfo was
classified against the joinrel fpinfo, whose
server/shippable_extensions aren't set until merge_fdw_options() runs
later — so extension-shippable quals would be misclassified as local
and silently kill the pushdown. Now it: (a) copies
shippable_extensions from the foreign side onto the stub, and (b)
classifies against the stub (fpinfo_i/fpinfo_o, which already has
server). Built-in quals like n > 3 were unaffected; this fixes the
extension case. I've added the test case for this.
2. Dropped unused funcid. The per-RTE metadata was a 3-element list
(funcid, funcrettype, funccollation) but funcid was never read.
Reduced to list_make2(funcrettype, funccollation); updated the
consumer (linitial/lsecond) and both descriptive comments.
3. Changed redundant elog(ERROR) into Assert in get_functions_data()
and get_tupdesc_for_join_scan_tuples(). The return-type validity is
already guaranteed by function_rte_pushdown_ok(), so these are pure
cross-checks.
4. Style consistency in find_em_for_rel_target(): introduced a named
fpinfo local instead of passing rel->fdw_private inline, matching
find_em_for_rel().
5. Also added test for nullable-side whole-row Var, but with outer
rows that both match and miss the (foreign x function) inner join.
------
Regards,
Alexander Korotkov
Supabase
Attachments:
[application/octet-stream] v10-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign.patch (79.6K, ../../CAPpHfdtxrvDkGsdxEnaGB4f2apHnHHS0BPySYgZ39gFoBt1F=g@mail.gmail.com/2-v10-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign.patch)
download | inline diff:
From 87e2bbb0e6554938d07a95c5f99e4afbb6f3e625 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 20 May 2026 09:22:51 +0300
Subject: [PATCH v10] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
---
contrib/postgres_fdw/deparse.c | 143 +++-
.../postgres_fdw/expected/postgres_fdw.out | 631 ++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 522 +++++++++++++--
contrib/postgres_fdw/postgres_fdw.h | 12 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 265 ++++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1553 insertions(+), 44 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..a2e9301fb65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -68,6 +68,7 @@ typedef struct foreign_glob_cxt
{
PlannerInfo *root; /* global planner state */
RelOptInfo *foreignrel; /* the foreign relation we are planning for */
+ PgFdwRelationInfo *fpinfo; /* the foreign server info we should rely on */
Relids relids; /* relids of base relations in the underlying
* scan */
} foreign_glob_cxt;
@@ -112,6 +113,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -217,6 +219,7 @@ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
void
classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds)
@@ -230,7 +233,7 @@ classifyConditions(PlannerInfo *root,
{
RestrictInfo *ri = lfirst_node(RestrictInfo, lc);
- if (is_foreign_expr(root, baserel, ri->clause))
+ if (is_foreign_expr(root, baserel, fpinfo, ri->clause))
*remote_conds = lappend(*remote_conds, ri);
else
*local_conds = lappend(*local_conds, ri);
@@ -243,11 +246,11 @@ classifyConditions(PlannerInfo *root,
bool
is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr)
{
foreign_glob_cxt glob_cxt;
foreign_loc_cxt loc_cxt;
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);
/*
* Check that the expression consists of nodes that are safe to execute
@@ -255,6 +258,7 @@ is_foreign_expr(PlannerInfo *root,
*/
glob_cxt.root = root;
glob_cxt.foreignrel = baserel;
+ glob_cxt.fpinfo = fpinfo;
/*
* For an upper relation, use relids from its underneath scan relation,
@@ -324,8 +328,8 @@ foreign_expr_walker(Node *node,
if (node == NULL)
return true;
- /* May need server info from baserel's fdw_private struct */
- fpinfo = (PgFdwRelationInfo *) (glob_cxt->foreignrel->fdw_private);
+ /* May need server info from global context */
+ fpinfo = glob_cxt->fpinfo;
/* Set up inner_cxt for possible recursion to child nodes */
inner_cxt.collation = InvalidOid;
@@ -2030,6 +2034,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2110,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2797,54 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ if (qualify_col)
+ {
+ appendStringInfoString(buf, "CASE WHEN (");
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfoString(buf, "*)::text IS NOT NULL THEN ");
+ }
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+
+ if (qualify_col)
+ appendStringInfoString(buf, " END");
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 0805c56cb1b..d1a9b82f32f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,637 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (Function g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (Function u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (Function u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a)) AND ((f1.c1 > 3))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+ QUERY PLAN
+-----------------------------------------------------------
+ Hash Join
+ Output: n.n, r.a, r.b
+ Hash Cond: (r.a = n.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4}'::integer[])
+ Filter: (n.n > f_local(3))
+(12 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+DROP FUNCTION f_local(int);
+-- Check that a qual on the function RTE using a function from a shippable
+-- extension is pushed down. plpgsql (not inlined) with the column as
+-- argument (not const-folded) keeps the function call in the qual.
+CREATE FUNCTION f_ext(int) RETURNS int AS $$
+BEGIN RETURN $1; END
+$$ LANGUAGE plpgsql IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f_ext(int);
+-- Force pushdown so the fixed-code plan is deterministically a Foreign
+-- Scan; on the unfixed code the qual is classified local, the join is
+-- refused, and the plan falls back to a local join instead.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and f_ext(n) > 3;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (Function n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a)) AND ((public.f_ext(f1.c1) > 3))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and f_ext(n) > 3 ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ext(int);
+DROP FUNCTION f_ext(int);
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (Function t)
+ Remote SQL: SELECT CASE WHEN (f2.*)::text IS NOT NULL THEN ROW(f2.c1, f2.c2) END, r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: u.*, ft5.c1, ft5.c2, ft4.c1, ft4.c2
+ Relations: (public.ft5) LEFT JOIN ((public.ft4) INNER JOIN (Function u))
+ Remote SQL: SELECT CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END, r1.c1, r1.c2, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 LEFT JOIN ("S 1"."T 3" r2 INNER JOIN ROWS FROM (unnest('{1,2}'::integer[]), unnest('{3,4}'::integer[])) f3(c1, c2) ON (((r2.c1 = f3.c1)))) ON (((r1.c1 = r2.c1)))) WHERE ((r1.c1 < 10)) ORDER BY r1.c1 ASC NULLS LAST
+(4 rows)
+
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ u | c1 | c2 | c1 | c2
+---+----+----+----+----
+ | 3 | 4 | |
+ | 6 | 7 | |
+ | 9 | 10 | |
+(3 rows)
+
+-- Same nullable-side whole-row Var, but with outer rows that both match
+-- and miss the (foreign x function) inner join. A multi-column function
+-- RTE is used so that the reference is a genuine whole-row Var (a single
+-- scalar-returning function would collapse to its lone column). This
+-- exercises the THEN ROW(...) side of deparseColumnRef()'s whole-row CASE
+-- (matched rows reconstruct the composite) as well as the NULL side
+-- (missed rows), confirming the executor rebuilds the anonymous composite
+-- correctly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: l.a, t.*
+ Relations: (public.remote_tbl l) LEFT JOIN ((public.remote_tbl r) INNER JOIN (Function t))
+ Remote SQL: SELECT r1.a, CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END FROM (public.base_tbl_fn r1 LEFT JOIN (public.base_tbl_fn r2 INNER JOIN ROWS FROM (unnest('{2,4}'::integer[]), unnest('{20,40}'::integer[])) f3(c1, c2) ON (((r2.a = f3.c1)))) ON (((r1.a = r2.a)))) WHERE ((r1.a < 6)) ORDER BY r1.a ASC NULLS LAST
+(4 rows)
+
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+ a | t
+---+--------
+ 1 |
+ 2 | (2,20)
+ 3 |
+ 4 | (4,40)
+ 5 |
+(5 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: s.a, s.b
+ Sort Key: s.a
+ CTE s
+ -> Nested Loop
+ Output: r1.a, r1.b
+ -> Foreign Scan on public.remote_tbl r1
+ Output: r1.a, r1.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Foreign Scan
+ Output: NULL::integer
+ Relations: (public.remote_tbl r2) INNER JOIN (Function f)
+ Remote SQL: SELECT NULL FROM (public.base_tbl_fn r1 INNER JOIN public.f($1::integer) f2(c1) ON (TRUE)) LIMIT 1::bigint
+ -> CTE Scan on s
+ Output: s.a, s.b
+(15 rows)
+
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ a | b
+----+-----
+ 1 | 101
+ 2 | 102
+ 3 | 103
+ 4 | 104
+ 5 | 105
+ 6 | 106
+ 7 | 107
+ 8 | 108
+ 9 | 109
+ 10 | 110
+ 11 | 111
+ 12 | 112
+ 13 | 113
+ 14 | 114
+ 15 | 115
+ 16 | 116
+ 17 | 117
+ 18 | 118
+ 19 | 119
+ 20 | 120
+ 21 | 121
+ 22 | 122
+ 23 | 123
+ 24 | 7
+ 25 | 125
+ 26 | 126
+ 27 | 127
+ 28 | 128
+ 29 | 129
+ 30 | 130
+(30 rows)
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6fa45773c30..c086f9b9adc 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of two-element lists (funcrettype, funccollation) -- one inner
+ * list per function in the RTE. Allows the executor to rebuild TupleDesc
+ * entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -741,6 +767,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -894,7 +923,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* Identify which baserestrictinfo clauses can be sent to the remote
* server and which can't.
*/
- classifyConditions(root, baserel, baserel->baserestrictinfo,
+ classifyConditions(root, baserel, fpinfo, baserel->baserestrictinfo,
&fpinfo->remote_conds, &fpinfo->local_conds);
/*
@@ -1298,7 +1327,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1374,7 +1403,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1514,7 +1543,7 @@ postgresGetForeignPlan(PlannerInfo *root,
remote_exprs = lappend(remote_exprs, rinfo->clause);
else if (list_member_ptr(fpinfo->local_conds, rinfo))
local_exprs = lappend(local_exprs, rinfo->clause);
- else if (is_foreign_expr(root, foreignrel, rinfo->clause))
+ else if (is_foreign_expr(root, foreignrel, fpinfo, rinfo->clause))
remote_exprs = lappend(remote_exprs, rinfo->clause);
else
local_exprs = lappend(local_exprs, rinfo->clause);
@@ -1635,9 +1664,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1658,9 +1704,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1696,13 +1748,67 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ *
+ * For an upperrel scan (the only path that reaches here)
+ * postgresGetForeignPlan always builds rtfuncdata via
+ * get_functions_data(), so it is never NIL; the per-RTE slot for
+ * the function RTE referenced by this Var must likewise be
+ * populated.
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ Assert(rtfuncdata != NIL);
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ Assert(funcdata != NIL);
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = linitial_node(Integer, fdata)->ival;
+ funccollation = lsecond_node(Integer, fdata)->ival;
+
+ /* get_functions_data() already validated the return type. */
+ Assert(OidIsValid(funcrettype) && funcrettype != RECORDOID);
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1738,14 +1844,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1788,8 +1910,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2742,7 +2875,7 @@ postgresPlanDirectModify(PlannerInfo *root,
if (attno <= InvalidAttrNumber) /* shouldn't happen */
elog(ERROR, "system-column update is not supported");
- if (!is_foreign_expr(root, foreignrel, (Expr *) tle->expr))
+ if (!is_foreign_expr(root, foreignrel, fpinfo, (Expr *) tle->expr))
return false;
}
}
@@ -2829,6 +2962,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2942,7 +3079,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3055,7 +3211,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3105,6 +3262,22 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
+
+ /*
+ * If a function RTE was absorbed into the foreign join,
+ * render it as "Function <alias>" since we have no foreign
+ * relid.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ refname = (char *) list_nth(es->rtable_names, rti - 1);
+ if (refname == NULL)
+ refname = rte->eref->aliasname;
+ appendStringInfo(&relations, "Function %s",
+ quote_identifier(refname));
+ continue;
+ }
+
Assert(rte->rtekind == RTE_RELATION);
/* This logic should agree with explain.c's ExplainTargetRel */
relname = get_rel_name(rte->relid);
@@ -3347,7 +3520,7 @@ estimate_path_cost_size(PlannerInfo *root,
* param_join_conds might contain both clauses that are safe to send
* across, and clauses that aren't.
*/
- classifyConditions(root, foreignrel, param_join_conds,
+ classifyConditions(root, foreignrel, fpinfo, param_join_conds,
&remote_param_join_conds, &local_param_join_conds);
/* Build the list of columns to be fetched from the foreign server. */
@@ -3480,8 +3653,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6639,6 +6821,194 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of two Integer nodes:
+ * (funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ /*
+ * function_rte_pushdown_ok() already rejected any function that
+ * doesn't return a well-defined scalar type, so this is a mere
+ * cross-check.
+ */
+ Assert(OidIsValid(funcrettype) && funcrettype != RECORDOID);
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ funcdata = lappend(funcdata,
+ list_make2(makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+
+ /*
+ * build_simple_rel() copies rtekind straight from the RTE, so for a base
+ * rel rel->rtekind always matches the RTE's; the check above is therefore
+ * sufficient.
+ */
+ Assert(rte->rtekind == RTE_FUNCTION);
+
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ *
+ * Note this rejects only lateral references to another relation at the
+ * same query level. A function argument referencing an outer query level
+ * (e.g. f(outer.col) inside a subquery) is a different case: it becomes a
+ * PARAM_EXEC Param, the function rel's lateral_relids is empty, and
+ * foreign_expr_walker() intentionally treats PARAM_EXEC as shippable.
+ * The remote query then carries a parameter placeholder, and
+ * postgres_fdw's ordinary parameter machinery re-sends it on each rescan
+ * -- i.e. it works as a normal parameterized foreign scan, which is fine.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, fdwrel->fdw_private, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6654,6 +7024,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6672,15 +7044,86 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->shippable_extensions = fpinfo_o->shippable_extensions;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+
+ /*
+ * Classify the function rel's own baserestrictinfo now, so that the
+ * local_conds check below can bail out if any of it is unshippable.
+ * We classify against the stub, not the joinrel's fpinfo, because the
+ * latter's server/shippable_extensions aren't populated until
+ * merge_fdw_options() runs further down.
+ */
+ classifyConditions(root, innerrel, fpinfo_i, innerrel->baserestrictinfo,
+ &fpinfo_i->remote_conds, &fpinfo_i->local_conds);
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->shippable_extensions = fpinfo_i->shippable_extensions;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+
+ /* See the comment in the inner_is_function branch above. */
+ classifyConditions(root, outerrel, fpinfo_o, outerrel->baserestrictinfo,
+ &fpinfo_o->remote_conds, &fpinfo_o->local_conds);
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
@@ -6718,7 +7161,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
foreach(lc, extra->restrictlist)
{
RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
- bool is_remote_clause = is_foreign_expr(root, joinrel,
+ bool is_remote_clause = is_foreign_expr(root, joinrel, fpinfo,
rinfo->clause);
if (IS_OUTER_JOIN(jointype) &&
@@ -7416,7 +7859,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* If any GROUP BY expression is not shippable, then we cannot
* push down aggregation to the foreign server.
*/
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
/*
@@ -7444,7 +7887,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* Non-grouping expression we need to compute. Can we ship it
* as-is to the foreign server?
*/
- if (is_foreign_expr(root, grouped_rel, expr) &&
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr) &&
!is_foreign_param(root, grouped_rel, expr))
{
/* Yes, so add to tlist as-is; OK to suppress duplicates */
@@ -7464,7 +7907,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* don't have to check is_foreign_param, since that certainly
* won't return true for any such expression.)
*/
- if (!is_foreign_expr(root, grouped_rel, (Expr *) aggvars))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, (Expr *) aggvars))
return false;
/*
@@ -7516,7 +7959,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
grouped_rel->relids,
NULL,
NULL);
- if (is_foreign_expr(root, grouped_rel, expr))
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr))
fpinfo->remote_conds = lappend(fpinfo->remote_conds, rinfo);
else
fpinfo->local_conds = lappend(fpinfo->local_conds, rinfo);
@@ -7553,7 +7996,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
*/
if (IsA(expr, Aggref))
{
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
tlist = add_to_flat_tlist(tlist, list_make1(expr));
@@ -8066,8 +8509,8 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel,
* Also, the LIMIT/OFFSET cannot be pushed down, if their expressions are
* not safe to remote.
*/
- if (!is_foreign_expr(root, input_rel, (Expr *) parse->limitOffset) ||
- !is_foreign_expr(root, input_rel, (Expr *) parse->limitCount))
+ if (!is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitOffset) ||
+ !is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitCount))
return;
/* Safe to push down */
@@ -8713,7 +9156,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
- is_foreign_expr(root, rel, em->em_expr))
+ is_foreign_expr(root, rel, fpinfo, em->em_expr))
return em;
}
@@ -8735,6 +9178,7 @@ EquivalenceMember *
find_em_for_rel_target(PlannerInfo *root, EquivalenceClass *ec,
RelOptInfo *rel)
{
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
PathTarget *target = rel->reltarget;
ListCell *lc1;
int i;
@@ -8784,7 +9228,7 @@ find_em_for_rel_target(PlannerInfo *root, EquivalenceClass *ec,
continue;
/* Check that expression (including relabels!) is shippable */
- if (is_foreign_expr(root, rel, em->em_expr))
+ if (is_foreign_expr(root, rel, fpinfo, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..da7da1c2ea9 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
@@ -183,11 +193,13 @@ extern char *pgfdw_application_name;
/* in deparse.c */
extern void classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds);
extern bool is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr);
extern bool is_foreign_param(PlannerInfo *root,
RelOptInfo *baserel,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 8162c5496bf..3cbf8339602 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,271 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+
+DROP FUNCTION f_local(int);
+
+-- Check that a qual on the function RTE using a function from a shippable
+-- extension is pushed down. plpgsql (not inlined) with the column as
+-- argument (not const-folded) keeps the function call in the qual.
+CREATE FUNCTION f_ext(int) RETURNS int AS $$
+BEGIN RETURN $1; END
+$$ LANGUAGE plpgsql IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f_ext(int);
+-- Force pushdown so the fixed-code plan is deterministically a Foreign
+-- Scan; on the unfixed code the qual is classified local, the join is
+-- refused, and the plan falls back to a local join instead.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and f_ext(n) > 3;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and f_ext(n) > 3 ORDER BY r.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ext(int);
+DROP FUNCTION f_ext(int);
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+
+-- Same nullable-side whole-row Var, but with outer rows that both match
+-- and miss the (foreign x function) inner join. A multi-column function
+-- RTE is used so that the reference is a genuine whole-row Var (a single
+-- scalar-returning function would collapse to its lone column). This
+-- exercises the THEN ROW(...) side of deparseColumnRef()'s whole-row CASE
+-- (matched rows reconstruct the composite) as well as the NULL side
+-- (missed rows), confirming the executor rebuilds the anonymous composite
+-- correctly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 687e923c46c..4e70686ad07 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 14+ messages in thread
* Re: Function scan FDW pushdown
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 18:21 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-20 10:17 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-07-06 14:11 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
@ 2026-07-07 10:25 ` Alexander Pyhalov <[email protected]>
0 siblings, 0 replies; 14+ messages in thread
From: Alexander Pyhalov @ 2026-07-07 10:25 UTC (permalink / raw)
To: Alexander Korotkov <[email protected]>; +Cc: solaimurugan vellaipandiyan <[email protected]>; Álvaro Herrera <[email protected]>; [email protected]; Ashutosh Bapat <[email protected]>; PostgreSQL Hackers <[email protected]>
Alexander Korotkov писал(а) 2026-07-06 17:11:
> Hi, Alexander!
>
> On Wed, May 20, 2026 at 1:17 PM Alexander Pyhalov
> <[email protected]> wrote:
>> I've found another issue. The fact that in the new versions of the
>> patch
>> RTE RelOptInfo misses fdw_private seems to be unfortunate. For
>> example,
>> in the last version we haven't thought about classifying
>> baserestrictinfo. And if we do, we should pass fdw_private down to
>> foreign_expr_walker. Perhaps, we could attach it to RTE_FUNCTION rel
>> prior to calling classifyConditions(), but should we later set it back
>> to NULL? Another problem comes if we try to handle joins, which can
>> crearte subqueries (like INNER/OUTER UNIQUE). In this case we should
>> somehow cook fpinfo for get_relation_column_alias_ids(). Attaching
>> patch
>> which tries to handle baserestrictinfos by passing fpinfo down to
>> foreign_expr_walker().
>
> I think passing fpinfo down to foreign_expr_walker() is the right
> approach, thank you. Attaching it to RTE_FUNCTION rel, and reseting
> it back to NULL would create excessive state and potential source of
> bugs. The only advantage is saving existing function signatures, but
> it doesn't worth it.
>
> Also, I don't think we need to care bout
> get_relation_column_alias_ids(). As we currently pushdown function
> only for inner joins, make_outerrel_subquery/make_innerrel_subquery
> are always false, lower_subquery_rels also wouldn't contain
> RTE_FUNCTION relid.
> Therefore:
>
> * In deparseFromExprForRel() for functional rel deparseRangeTblRef()
> is always called with make_subquery=false.
> * is_subquery_var() returns false.
Well, at least current patch version classifies conditions correctly.
> Other changes I made:
>
> 1. In foreign_join_ok(), the function side's baserestrictinfo was
> classified against the joinrel fpinfo, whose
> server/shippable_extensions aren't set until merge_fdw_options() runs
> later — so extension-shippable quals would be misclassified as local
> and silently kill the pushdown. Now it: (a) copies
> shippable_extensions from the foreign side onto the stub, and (b)
> classifies against the stub (fpinfo_i/fpinfo_o, which already has
> server). Built-in quals like n > 3 were unaffected; this fixes the
> extension case. I've added the test case for this.
>
> 2. Dropped unused funcid. The per-RTE metadata was a 3-element list
> (funcid, funcrettype, funccollation) but funcid was never read.
> Reduced to list_make2(funcrettype, funccollation); updated the
> consumer (linitial/lsecond) and both descriptive comments.
With funcid we could print actual function name in explain.
Now we print, for example,
Relations: ((public.distr1_part_3 d1_3) INNER JOIN (public.distr2_part_3
d2_3)) INNER JOIN (Function n)
If funcid is preserved, we could output actual function name instead of
alias. This is more similar to
how we show relation names. Attaching updated patch, which preserves
function names in explain.
After looking at it once more, I'm not convinced when we can get
not-FuncExpr in rtfunc->funcexpr.
So added more checks to function_rte_pushdown_ok() to avoid dealing with
such case.
--
Best regards,
Alexander Pyhalov,
Postgres Professional
Attachments:
[text/x-diff] v11-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign.patch (82.5K, ../../[email protected]/2-v11-0001-postgres_fdw-push-down-FUNCTION-RTE-into-foreign.patch)
download | inline diff:
From b850713b648ed985816c733552db40d613294005 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Tue, 7 Jul 2026 09:57:19 +0300
Subject: [PATCH] postgres_fdw: push down FUNCTION RTE into foreign joins
A foreign join planning hook now considers a (foreign-table x function-RTE)
INNER join as a push-down candidate when the function expression is
IMMUTABLE and otherwise shippable. The remote query absorbs the function
call as a FROM-list item (e.g. unnest(...) AS f<rti>(c1, c2, ...)), so the
foreign side returns only rows that match the function-produced set and the
join executes entirely on the remote.
An IMMUTABLE function gives the same result on any server, so the same
function RTE can be a push-down candidate for several distinct foreign
servers without semantic risk. To keep the planner state consistent
across those independent attempts, the per-call stub fpinfo for the
function side lives on the joinrel's PgFdwRelationInfo (new
outer_func_fpinfo / inner_func_fpinfo), never on the function rel itself,
and the function side is detected via rtekind rather than fdw_private.
set_foreign_rel_properties() propagates fdwroutine onto a joinrel that
pairs a foreign rel with an RTE_FUNCTION rel so GetForeignJoinPaths gets
called; the FDW retains full control over whether to actually generate a
path. deparseRangeTblRef and deparseColumnRef gain a FUNCTION-RTE branch
that emits the function expression and resolves Vars to the generated
---
contrib/postgres_fdw/deparse.c | 143 +++-
.../postgres_fdw/expected/postgres_fdw.out | 631 ++++++++++++++++++
contrib/postgres_fdw/postgres_fdw.c | 608 +++++++++++++++--
contrib/postgres_fdw/postgres_fdw.h | 12 +
contrib/postgres_fdw/sql/postgres_fdw.sql | 265 ++++++++
src/backend/optimizer/util/relnode.c | 24 +
6 files changed, 1626 insertions(+), 57 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 2dcc6c8af1b..a2e9301fb65 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -68,6 +68,7 @@ typedef struct foreign_glob_cxt
{
PlannerInfo *root; /* global planner state */
RelOptInfo *foreignrel; /* the foreign relation we are planning for */
+ PgFdwRelationInfo *fpinfo; /* the foreign server info we should rely on */
Relids relids; /* relids of base relations in the underlying
* scan */
} foreign_glob_cxt;
@@ -112,6 +113,7 @@ typedef struct deparse_expr_cxt
appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
#define SUBQUERY_REL_ALIAS_PREFIX "s"
#define SUBQUERY_COL_ALIAS_PREFIX "c"
+#define FUNCTION_REL_ALIAS_PREFIX "f"
/*
* Functions to determine whether an expression can be evaluated safely on
@@ -217,6 +219,7 @@ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
void
classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds)
@@ -230,7 +233,7 @@ classifyConditions(PlannerInfo *root,
{
RestrictInfo *ri = lfirst_node(RestrictInfo, lc);
- if (is_foreign_expr(root, baserel, ri->clause))
+ if (is_foreign_expr(root, baserel, fpinfo, ri->clause))
*remote_conds = lappend(*remote_conds, ri);
else
*local_conds = lappend(*local_conds, ri);
@@ -243,11 +246,11 @@ classifyConditions(PlannerInfo *root,
bool
is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr)
{
foreign_glob_cxt glob_cxt;
foreign_loc_cxt loc_cxt;
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);
/*
* Check that the expression consists of nodes that are safe to execute
@@ -255,6 +258,7 @@ is_foreign_expr(PlannerInfo *root,
*/
glob_cxt.root = root;
glob_cxt.foreignrel = baserel;
+ glob_cxt.fpinfo = fpinfo;
/*
* For an upper relation, use relids from its underneath scan relation,
@@ -324,8 +328,8 @@ foreign_expr_walker(Node *node,
if (node == NULL)
return true;
- /* May need server info from baserel's fdw_private struct */
- fpinfo = (PgFdwRelationInfo *) (glob_cxt->foreignrel->fdw_private);
+ /* May need server info from global context */
+ fpinfo = glob_cxt->fpinfo;
/* Set up inner_cxt for possible recursion to child nodes */
inner_cxt.collation = InvalidOid;
@@ -2030,6 +2034,72 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
}
}
+/*
+ * Deparse a FUNCTION RTE absorbed into a foreign join. The function call(s)
+ * are emitted as a FROM-list item:
+ *
+ * <funcexpr> AS f<rti>(c1, c2, ..., cN)
+ *
+ * For multi-function RTEs (SQL ROWS FROM (f1(), f2(), ...)), each
+ * function call appears comma-separated inside ROWS FROM(...). Column
+ * aliases c1..cN cover the union of every function's columns, in the
+ * order they appear; that matches the column ordering of the RTE.
+ */
+static void
+deparseFunctionRangeTblRef(StringInfo buf, PlannerInfo *root,
+ RelOptInfo *foreignrel, RelOptInfo *scanrel,
+ List **params_list)
+{
+ RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
+ deparse_expr_cxt context;
+ ListCell *lc;
+ bool multi_func;
+ int total_cols = 0;
+ int i;
+
+ Assert(rte->rtekind == RTE_FUNCTION);
+ Assert(!rte->funcordinality);
+ Assert(list_length(rte->functions) >= 1);
+
+ multi_func = list_length(rte->functions) > 1;
+
+ context.buf = buf;
+ context.root = root;
+ context.foreignrel = scanrel;
+ context.scanrel = scanrel;
+ context.params_list = params_list;
+
+ if (multi_func)
+ appendStringInfoString(buf, "ROWS FROM (");
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+
+ if (foreach_current_index(lc) > 0)
+ appendStringInfoString(buf, ", ");
+ deparseExpr((Expr *) rtfunc->funcexpr, &context);
+ total_cols += rtfunc->funccolcount;
+ }
+
+ if (multi_func)
+ appendStringInfoChar(buf, ')');
+
+ /* Alias + generated column-name list. */
+ appendStringInfo(buf, " %s%d", FUNCTION_REL_ALIAS_PREFIX, foreignrel->relid);
+ if (total_cols > 0)
+ {
+ appendStringInfoChar(buf, '(');
+ for (i = 1; i <= total_cols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+ }
+}
+
/*
* Append FROM clause entry for the given relation into buf.
* Conditions from lower-level SEMI-JOINs are appended to additional_conds
@@ -2040,7 +2110,22 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
bool make_subquery, Index ignore_rel, List **ignore_conds,
List **additional_conds, List **params_list)
{
- PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ PgFdwRelationInfo *fpinfo;
+
+ /*
+ * For a function RTE absorbed into a foreign join, deparse the function
+ * expression as a FROM-list item and return. The stub fpinfo set up by
+ * foreign_join_ok() may or may not be present here.
+ */
+ if (foreignrel->rtekind == RTE_FUNCTION)
+ {
+ Assert(!make_subquery);
+ deparseFunctionRangeTblRef(buf, root, foreignrel, foreignrel,
+ params_list);
+ return;
+ }
+
+ fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
/* Should only be called in these cases. */
Assert(IS_SIMPLE_REL(foreignrel) || IS_JOIN_REL(foreignrel));
@@ -2712,6 +2797,54 @@ static void
deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
bool qualify_col)
{
+ /*
+ * Function RTE columns: emit as f<varno>.c<varattno>, matching the
+ * aliases generated by deparseFunctionRangeTblRef(). A whole-row Var
+ * (varattno == 0) is rendered as ROW(f<varno>.c1, ..., f<varno>.c<N>)
+ * where N is the total column count of the function RTE (including all
+ * ROWS FROM (...) members). System attributes such as ctid have no
+ * meaning for function RTEs and are rejected.
+ */
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ if (varattno == 0)
+ {
+ int ncols = list_length(rte->eref->colnames);
+ int i;
+
+ if (qualify_col)
+ {
+ appendStringInfoString(buf, "CASE WHEN (");
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfoString(buf, "*)::text IS NOT NULL THEN ");
+ }
+
+ appendStringInfoString(buf, "ROW(");
+ for (i = 1; i <= ncols; i++)
+ {
+ if (i > 1)
+ appendStringInfoString(buf, ", ");
+ appendStringInfo(buf, "%s%d.%s%d",
+ FUNCTION_REL_ALIAS_PREFIX, varno,
+ SUBQUERY_COL_ALIAS_PREFIX, i);
+ }
+ appendStringInfoChar(buf, ')');
+
+ if (qualify_col)
+ appendStringInfoString(buf, " END");
+ return;
+ }
+
+ if (varattno < 0)
+ elog(ERROR,
+ "system attribute reference to a function RTE is not supported in foreign join pushdown");
+
+ if (qualify_col)
+ appendStringInfo(buf, "%s%d.", FUNCTION_REL_ALIAS_PREFIX, varno);
+ appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, varattno);
+ return;
+ }
+
/* We support fetching the remote side's CTID and OID. */
if (varattno == SelfItemPointerAttributeNumber)
{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index aaffcf31271..e3355aa8a86 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2885,6 +2885,637 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
(10 rows)
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1, t1.c3
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1, t1.c3
+ Relations: (public.ft1 t1) INNER JOIN (pg_catalog.unnest() u)
+ Remote SQL: SELECT r1."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{1,5,10,100}'::integer[]) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+ c1 | c3
+-----+-------
+ 1 | 00001
+ 5 | 00005
+ 10 | 00010
+ 100 | 00100
+(4 rows)
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: t1.c1
+ Sort Key: t1.c1
+ -> Foreign Scan
+ Output: t1.c1
+ Relations: (public.ft1 t1) INNER JOIN (pg_catalog.generate_series() g)
+ Remote SQL: SELECT r1."C 1" FROM ("S 1"."T 1" r1 INNER JOIN generate_series(1, 4) f2(c1) ON (((r1."C 1" = f2.c1))))
+(7 rows)
+
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+ c1
+----
+ 1
+ 2
+ 3
+ 4
+(4 rows)
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1
+ Hash Cond: (g.id = t1.c1)
+ -> Function Scan on pg_catalog.generate_series g
+ Output: g.id
+ Function Call: generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Hash
+ Output: t1.c1
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+(11 rows)
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+ QUERY PLAN
+------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, u.ord
+ Hash Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ -> Hash
+ Output: u.ord, u.id
+ -> Function Scan on pg_catalog.unnest u
+ Output: u.ord, u.id
+ Function Call: unnest('{1,5,10}'::integer[])
+(11 rows)
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: t1.c1, t2.c1
+ Merge Cond: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC NULLS LAST
+ -> Sort
+ Output: t2.c1, u.id
+ Sort Key: t2.c1
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (pg_catalog.unnest() u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{1,5,10,100}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(13 rows)
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+ Output: t1.c1, t2.c1
+ Hash Cond: (t1.c1 = t2.c1)
+ -> Foreign Scan
+ Output: t1.c1, u.id
+ Relations: (public.ft1 t1) INNER JOIN (pg_catalog.unnest() u)
+ Remote SQL: SELECT r1."C 1", f3.c1 FROM ("S 1"."T 1" r1 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r1."C 1" = f3.c1))))
+ -> Hash
+ Output: t2.c1
+ -> Foreign Scan on public.ft6 t2
+ Output: t2.c1
+ Remote SQL: SELECT c1 FROM "S 1"."T 4"
+(12 rows)
+
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: t1.c1, t2.c1
+ Join Filter: (t1.c1 = u.id)
+ -> Foreign Scan on public.ft1 t1
+ Output: t1.c1
+ Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE ((c3 < '00010'))
+ -> Materialize
+ Output: t2.c1, u.id
+ -> Foreign Scan
+ Output: t2.c1, u.id
+ Relations: (public.ft6 t2) INNER JOIN (pg_catalog.unnest() u)
+ Remote SQL: SELECT r2.c1, f3.c1 FROM ("S 1"."T 4" r2 INNER JOIN unnest('{3,6,9,12,15,18}'::integer[]) f3(c1) ON (((r2.c1 = f3.c1))))
+(12 rows)
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (pg_catalog.unnest() n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 2 | 2 | 102
+ 3 | 3 | 103
+ 4 | 4 | 104
+(3 rows)
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (pg_catalog.unnest() n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a)) AND ((f1.c1 > 3))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+ QUERY PLAN
+-----------------------------------------------------------
+ Hash Join
+ Output: n.n, r.a, r.b
+ Hash Cond: (r.a = n.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Hash
+ Output: n.n
+ -> Function Scan on pg_catalog.unnest n
+ Output: n.n
+ Function Call: unnest('{2,3,4}'::integer[])
+ Filter: (n.n > f_local(3))
+(12 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+DROP FUNCTION f_local(int);
+-- Check that a qual on the function RTE using a function from a shippable
+-- extension is pushed down. plpgsql (not inlined) with the column as
+-- argument (not const-folded) keeps the function call in the qual.
+CREATE FUNCTION f_ext(int) RETURNS int AS $$
+BEGIN RETURN $1; END
+$$ LANGUAGE plpgsql IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f_ext(int);
+-- Force pushdown so the fixed-code plan is deterministically a Foreign
+-- Scan; on the unfixed code the qual is classified local, the join is
+-- refused, and the plan falls back to a local join instead.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and f_ext(n) > 3;
+ QUERY PLAN
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: n.n, r.a, r.b
+ Relations: (pg_catalog.unnest() n) INNER JOIN (public.remote_tbl r)
+ Remote SQL: SELECT f1.c1, r2.a, r2.b FROM (unnest('{2,3,4}'::integer[]) f1(c1) INNER JOIN public.base_tbl_fn r2 ON (((f1.c1 = r2.a)) AND ((public.f_ext(f1.c1) > 3))))
+(4 rows)
+
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and f_ext(n) > 3 ORDER BY r.a;
+ n | a | b
+---+---+-----
+ 4 | 4 | 104
+(1 row)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ext(int);
+DROP FUNCTION f_ext(int);
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+ QUERY PLAN
+------------------------------------------------------
+ Hash Join
+ Output: s.*
+ Hash Cond: (rt.a = s.a)
+ -> Foreign Scan on public.remote_tbl rt
+ Output: rt.a, rt.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+ -> Hash
+ Output: s.*, s.a
+ -> Function Scan on public.f_ret_record s
+ Output: s.*, s.a
+ Function Call: f_ret_record()
+(11 rows)
+
+DROP FUNCTION f_ret_record();
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+ a | b
+----+-----
+ 23 | 123
+ 24 | 999
+ 25 | 125
+(3 rows)
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+ a | b
+----+---
+ 24 | 5
+(1 row)
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+ a | b | t
+----+---+---------------------
+ 24 | 7 | ("(2,3),(-2,-3)",1)
+(1 row)
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: r.a, t.n, t.s
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: r.a, t.n, t.s
+ Relations: (public.remote_tbl r) INNER JOIN (ROWS FROM (pg_catalog.unnest(), pg_catalog.generate_series()) t)
+ Remote SQL: SELECT r1.a, f2.c1, f2.c2 FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ a | n | s
+---+---+----
+ 3 | 3 | 11
+ 6 | 6 | 12
+ 9 | 9 | 13
+(3 rows)
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: ((t.*)::text), r.a
+ Sort Key: r.a
+ -> Foreign Scan
+ Output: (t.*)::text, r.a
+ Relations: (public.remote_tbl r) INNER JOIN (ROWS FROM (pg_catalog.unnest(), pg_catalog.generate_series()) t)
+ Remote SQL: SELECT CASE WHEN (f2.*)::text IS NOT NULL THEN ROW(f2.c1, f2.c2) END, r1.a FROM (public.base_tbl_fn r1 INNER JOIN ROWS FROM (unnest('{3,6,9}'::integer[]), generate_series(11, 13)) f2(c1, c2) ON (((r1.a = f2.c1))))
+(7 rows)
+
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+ t | a
+--------+---
+ (3,11) | 3
+ (6,12) | 6
+ (9,13) | 9
+(3 rows)
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: u.*, ft5.c1, ft5.c2, ft4.c1, ft4.c2
+ Relations: (public.ft5) LEFT JOIN ((public.ft4) INNER JOIN (ROWS FROM (pg_catalog.unnest(), pg_catalog.unnest()) u))
+ Remote SQL: SELECT CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END, r1.c1, r1.c2, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 LEFT JOIN ("S 1"."T 3" r2 INNER JOIN ROWS FROM (unnest('{1,2}'::integer[]), unnest('{3,4}'::integer[])) f3(c1, c2) ON (((r2.c1 = f3.c1)))) ON (((r1.c1 = r2.c1)))) WHERE ((r1.c1 < 10)) ORDER BY r1.c1 ASC NULLS LAST
+(4 rows)
+
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+ u | c1 | c2 | c1 | c2
+---+----+----+----+----
+ | 3 | 4 | |
+ | 6 | 7 | |
+ | 9 | 10 | |
+(3 rows)
+
+-- Same nullable-side whole-row Var, but with outer rows that both match
+-- and miss the (foreign x function) inner join. A multi-column function
+-- RTE is used so that the reference is a genuine whole-row Var (a single
+-- scalar-returning function would collapse to its lone column). This
+-- exercises the THEN ROW(...) side of deparseColumnRef()'s whole-row CASE
+-- (matched rows reconstruct the composite) as well as the NULL side
+-- (missed rows), confirming the executor rebuilds the anonymous composite
+-- correctly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+ Output: l.a, t.*
+ Relations: (public.remote_tbl l) LEFT JOIN ((public.remote_tbl r) INNER JOIN (ROWS FROM (pg_catalog.unnest(), pg_catalog.unnest()) t))
+ Remote SQL: SELECT r1.a, CASE WHEN (f3.*)::text IS NOT NULL THEN ROW(f3.c1, f3.c2) END FROM (public.base_tbl_fn r1 LEFT JOIN (public.base_tbl_fn r2 INNER JOIN ROWS FROM (unnest('{2,4}'::integer[]), unnest('{20,40}'::integer[])) f3(c1, c2) ON (((r2.a = f3.c1)))) ON (((r1.a = r2.a)))) WHERE ((r1.a < 6)) ORDER BY r1.a ASC NULLS LAST
+(4 rows)
+
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+ a | t
+---+--------
+ 1 |
+ 2 | (2,20)
+ 3 |
+ 4 | (4,40)
+ 5 |
+(5 rows)
+
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ QUERY PLAN
+----------------------------------------------------------------------------------------------------------------------------------------
+ Sort
+ Output: s.a, s.b
+ Sort Key: s.a
+ CTE s
+ -> Nested Loop
+ Output: r1.a, r1.b
+ -> Foreign Scan on public.remote_tbl r1
+ Output: r1.a, r1.b
+ Remote SQL: SELECT a, b FROM public.base_tbl_fn
+ -> Foreign Scan
+ Output: NULL::integer
+ Relations: (public.remote_tbl r2) INNER JOIN (public.f())
+ Remote SQL: SELECT NULL FROM (public.base_tbl_fn r1 INNER JOIN public.f($1::integer) f2(c1) ON (TRUE)) LIMIT 1::bigint
+ -> CTE Scan on s
+ Output: s.a, s.b
+(15 rows)
+
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+ a | b
+----+-----
+ 1 | 101
+ 2 | 102
+ 3 | 103
+ 4 | 104
+ 5 | 105
+ 6 | 106
+ 7 | 107
+ 8 | 108
+ 9 | 109
+ 10 | 110
+ 11 | 111
+ 12 | 112
+ 13 | 113
+ 14 | 114
+ 15 | 115
+ 16 | 116
+ 17 | 117
+ 18 | 118
+ 19 | 119
+ 20 | 120
+ 21 | 121
+ 22 | 122
+ 23 | 123
+ 24 | 7
+ 25 | 125
+ 26 | 126
+ 27 | 127
+ 28 | 128
+ 29 | 129
+ 30 | 130
+(30 rows)
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+ QUERY PLAN
+------------------------------------------------------------------------------------------------------------------------------------
+ Merge Join
+ Output: r.a
+ Merge Cond: (t.n = r.a)
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[]), generate_series(1, (4 + ((random() * '0'::double precision))::integer))
+ -> Sort
+ Output: r.a
+ Sort Key: r.a
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a
+ Remote SQL: SELECT a FROM public.base_tbl_fn
+(15 rows)
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Nested Loop
+ Output: r.a, t.x
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 3)) ORDER BY a ASC NULLS LAST
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.x
+ Function Call: unnest(ARRAY[r.a])
+(8 rows)
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+ QUERY PLAN
+-------------------------------------------------------------------------------------------------
+ Merge Left Join
+ Output: r.a, t.n
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn WHERE ((a <= 5)) ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{1,2,3}'::integer[])
+(12 rows)
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+ QUERY PLAN
+--------------------------------------------------------------------------------
+ Merge Semi Join
+ Output: r.a
+ Merge Cond: (r.a = t.n)
+ -> Foreign Scan on public.remote_tbl r
+ Output: r.a, r.b
+ Remote SQL: SELECT a FROM public.base_tbl_fn ORDER BY a ASC NULLS LAST
+ -> Sort
+ Output: t.n
+ Sort Key: t.n
+ -> Function Scan on pg_catalog.unnest t
+ Output: t.n
+ Function Call: unnest('{3,6,9}'::integer[])
+(12 rows)
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 0ff4ec23164..59af46f6942 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -31,6 +31,7 @@
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "optimizer/appendinfo.h"
+#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
@@ -88,6 +89,22 @@ enum FdwScanPrivateIndex
* of join, added when the scan is join
*/
FdwScanPrivateRelations,
+
+ /*
+ * List of per-RTE function metadata, indexed by base RTI offset. Each
+ * element is either NULL (for non-RTE_FUNCTION rels in this scan) or a
+ * list of three-element lists (funcid, funcrettype, funccollation) -- one
+ * inner list per function in the RTE. Allows the executor to rebuild
+ * TupleDesc entries for whole-row references to function RTEs.
+ */
+ FdwScanPrivateFunctions,
+
+ /*
+ * Integer node: minimum base RT index covered by the scan, used to
+ * translate scan-local indexes to estate-rtable indexes after setrefs.c
+ * flattens rtables.
+ */
+ FdwScanPrivateMinRTIndex,
};
/*
@@ -124,6 +141,11 @@ enum FdwModifyPrivateIndex
* 2) Boolean flag showing if the remote query has a RETURNING clause
* 3) Integer list of attribute numbers retrieved by RETURNING, if any
* 4) Boolean flag showing if we set the command es_processed
+ * 5) Per-RTE function metadata (mirrors FdwScanPrivateFunctions; lets
+ * the executor rebuild TupleDesc entries for whole-row Vars over
+ * function RTEs absorbed into a foreign join)
+ * 6) Integer node: minimum base RT index of the scan (mirrors
+ * FdwScanPrivateMinRTIndex)
*/
enum FdwDirectModifyPrivateIndex
{
@@ -135,6 +157,10 @@ enum FdwDirectModifyPrivateIndex
FdwDirectModifyPrivateRetrievedAttrs,
/* set-processed flag (as a Boolean node) */
FdwDirectModifyPrivateSetProcessed,
+ /* Per-RTE function metadata, indexed by base RTI offset */
+ FdwDirectModifyPrivateFunctions,
+ /* Integer node: minimum base RT index in the scan */
+ FdwDirectModifyPrivateMinRTIndex,
};
/*
@@ -740,6 +766,9 @@ static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
RelOptInfo *rel);
static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static Relids get_base_relids(PlannerInfo *root, RelOptInfo *rel);
+static int get_min_base_rti(PlannerInfo *root, RelOptInfo *rel);
+static List *get_functions_data(PlannerInfo *root, RelOptInfo *rel);
static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
Path *epq_path, List *restrictlist);
static void add_foreign_grouping_paths(PlannerInfo *root,
@@ -893,7 +922,7 @@ postgresGetForeignRelSize(PlannerInfo *root,
* Identify which baserestrictinfo clauses can be sent to the remote
* server and which can't.
*/
- classifyConditions(root, baserel, baserel->baserestrictinfo,
+ classifyConditions(root, baserel, fpinfo, baserel->baserestrictinfo,
&fpinfo->remote_conds, &fpinfo->local_conds);
/*
@@ -1297,7 +1326,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1373,7 +1402,7 @@ postgresGetForeignPaths(PlannerInfo *root,
continue;
/* See if it is safe to send to remote */
- if (!is_foreign_expr(root, baserel, rinfo->clause))
+ if (!is_foreign_expr(root, baserel, fpinfo, rinfo->clause))
continue;
/* Calculate required outer rels for the resulting path */
@@ -1513,7 +1542,7 @@ postgresGetForeignPlan(PlannerInfo *root,
remote_exprs = lappend(remote_exprs, rinfo->clause);
else if (list_member_ptr(fpinfo->local_conds, rinfo))
local_exprs = lappend(local_exprs, rinfo->clause);
- else if (is_foreign_expr(root, foreignrel, rinfo->clause))
+ else if (is_foreign_expr(root, foreignrel, fpinfo, rinfo->clause))
remote_exprs = lappend(remote_exprs, rinfo->clause);
else
local_exprs = lappend(local_exprs, rinfo->clause);
@@ -1634,9 +1663,26 @@ postgresGetForeignPlan(PlannerInfo *root,
fdw_private = list_make3(makeString(sql.data),
retrieved_attrs,
makeInteger(fpinfo->fetch_size));
+
+ /*
+ * Position FdwScanPrivateRelations: either the EXPLAIN relation string
+ * (joins/upper rels) or a NULL placeholder, so that subsequent indexes
+ * stay valid for the base-rel scan case.
+ */
if (IS_JOIN_REL(foreignrel) || IS_UPPER_REL(foreignrel))
fdw_private = lappend(fdw_private,
makeString(fpinfo->relation_name));
+ else
+ fdw_private = lappend(fdw_private, NULL);
+
+ /*
+ * FdwScanPrivateFunctions / FdwScanPrivateMinRTIndex carry the metadata
+ * the executor needs to rebuild TupleDesc entries for whole-row Vars
+ * pointing at RTE_FUNCTION rels absorbed into the foreign scan.
+ */
+ fdw_private = lappend(fdw_private, get_functions_data(root, foreignrel));
+ fdw_private = lappend(fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Create the ForeignScan node for the given relation.
@@ -1657,9 +1703,15 @@ postgresGetForeignPlan(PlannerInfo *root,
/*
* Construct a tuple descriptor for the scan tuples handled by a foreign join.
+ *
+ * 'rtfuncdata' is the FdwScanPrivateFunctions list saved at plan time, and
+ * 'rtoffset' is the difference between the executor's RT indexes and the
+ * scan-local RT indexes captured in that list. Both may be 0/NIL when the
+ * scan has no RTE_FUNCTION dependents.
*/
static TupleDesc
-get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
+get_tupdesc_for_join_scan_tuples(ForeignScanState *node,
+ List *rtfuncdata, int rtoffset)
{
ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
EState *estate = node->ss.ps.state;
@@ -1695,13 +1747,67 @@ get_tupdesc_for_join_scan_tuples(ForeignScanState *node)
if (!IsA(var, Var) || var->varattno != 0)
continue;
rte = list_nth(estate->es_range_table, var->varno - 1);
- if (rte->rtekind != RTE_RELATION)
- continue;
- reltype = get_rel_type_id(rte->relid);
- if (!OidIsValid(reltype))
- continue;
- att->atttypid = reltype;
- /* shouldn't need to change anything else */
+
+ if (rte->rtekind == RTE_RELATION)
+ {
+ reltype = get_rel_type_id(rte->relid);
+ if (!OidIsValid(reltype))
+ continue;
+ att->atttypid = reltype;
+ /* shouldn't need to change anything else */
+ }
+ else if (rte->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * A whole-row Var points at a FUNCTION RTE absorbed into the
+ * foreign join. Synthesize an anonymous composite TupleDesc from
+ * the per-function return-type metadata we saved at plan time;
+ * the deparser emits these as ROW(f<rti>.c1, f<rti>.c2, ...).
+ *
+ * For an upperrel scan (the only path that reaches here)
+ * postgresGetForeignPlan always builds rtfuncdata via
+ * get_functions_data(), so it is never NIL; the per-RTE slot for
+ * the function RTE referenced by this Var must likewise be
+ * populated.
+ */
+ List *funcdata;
+ TupleDesc rte_tupdesc;
+ int num_funcs;
+ int attnum;
+ ListCell *lc1,
+ *lc2;
+
+ Assert(rtfuncdata != NIL);
+ funcdata = list_nth(rtfuncdata, var->varno - rtoffset);
+ Assert(funcdata != NIL);
+ num_funcs = list_length(funcdata);
+ Assert(num_funcs == list_length(rte->eref->colnames));
+ rte_tupdesc = CreateTemplateTupleDesc(num_funcs);
+
+ attnum = 1;
+ forboth(lc1, funcdata, lc2, rte->eref->colnames)
+ {
+ List *fdata = lfirst_node(List, lc1);
+ char *colname = strVal(lfirst(lc2));
+ Oid funcrettype;
+ Oid funccollation;
+
+ funcrettype = lsecond_node(Integer, fdata)->ival;
+ funccollation = lthird_node(Integer, fdata)->ival;
+
+ /* get_functions_data() already validated the return type. */
+ Assert(OidIsValid(funcrettype) && funcrettype != RECORDOID);
+
+ TupleDescInitEntry(rte_tupdesc, (AttrNumber) attnum, colname,
+ funcrettype, -1, 0);
+ TupleDescInitEntryCollation(rte_tupdesc, (AttrNumber) attnum,
+ funccollation);
+ attnum++;
+ }
+
+ assign_record_type_typmod(rte_tupdesc);
+ att->atttypmod = rte_tupdesc->tdtypmod;
+ }
}
return tupdesc;
}
@@ -1737,14 +1843,30 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
/*
* Identify which user to do the remote access as. This should match what
- * ExecCheckPermissions() does.
+ * ExecCheckPermissions() does. For a join, scan the base relids until we
+ * find an RTE_RELATION (the foreign-table side); ignore any RTE_FUNCTION
+ * absorbed into the join, which contributes no relation OID to look up.
*/
userid = OidIsValid(fsplan->checkAsUser) ? fsplan->checkAsUser : GetUserId();
+ rte = NULL;
if (fsplan->scan.scanrelid > 0)
+ {
rtindex = fsplan->scan.scanrelid;
+ rte = exec_rt_fetch(rtindex, estate);
+ }
else
- rtindex = bms_next_member(fsplan->fs_base_relids, -1);
- rte = exec_rt_fetch(rtindex, estate);
+ {
+ rtindex = -1;
+ while ((rtindex = bms_next_member(fsplan->fs_base_relids, rtindex)) >= 0)
+ {
+ rte = exec_rt_fetch(rtindex, estate);
+ if (rte != NULL && rte->rtekind == RTE_RELATION)
+ break;
+ rte = NULL;
+ }
+ if (rte == NULL)
+ elog(ERROR, "could not locate a foreign relation RTE in foreign scan");
+ }
/* Get info about foreign table. */
table = GetForeignTable(rte->relid);
@@ -1787,8 +1909,19 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags)
}
else
{
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwScanPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwScanPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
fsstate->rel = NULL;
- fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ fsstate->tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata,
+ rtoffset);
}
fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
@@ -2741,7 +2874,7 @@ postgresPlanDirectModify(PlannerInfo *root,
if (attno <= InvalidAttrNumber) /* shouldn't happen */
elog(ERROR, "system-column update is not supported");
- if (!is_foreign_expr(root, foreignrel, (Expr *) tle->expr))
+ if (!is_foreign_expr(root, foreignrel, fpinfo, (Expr *) tle->expr))
return false;
}
}
@@ -2828,6 +2961,10 @@ postgresPlanDirectModify(PlannerInfo *root,
makeBoolean((retrieved_attrs != NIL)),
retrieved_attrs,
makeBoolean(plan->canSetTag));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ get_functions_data(root, foreignrel));
+ fscan->fdw_private = lappend(fscan->fdw_private,
+ makeInteger(get_min_base_rti(root, foreignrel)));
/*
* Update the foreign-join-related fields.
@@ -2941,7 +3078,26 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
TupleDesc tupdesc;
if (fsplan->scan.scanrelid == 0)
- tupdesc = get_tupdesc_for_join_scan_tuples(node);
+ {
+ /*
+ * DirectModify on a foreign join: use the per-RTE function
+ * metadata saved at plan time so a whole-row Var pointing at a
+ * function RTE absorbed into the join can be rebuilt into a
+ * usable TupleDesc (e.g. RETURNING t for a join with UNNEST(...,
+ * ...) AS t(bx, i)).
+ */
+ List *rtfuncdata = (List *) list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateFunctions);
+ int min_base_rti = intVal(list_nth(fsplan->fdw_private,
+ FdwDirectModifyPrivateMinRTIndex));
+ int rtoffset = bms_next_member(fsplan->fs_base_relids, -1) -
+ min_base_rti;
+
+ Assert(min_base_rti > 0);
+ Assert(rtoffset >= 0);
+
+ tupdesc = get_tupdesc_for_join_scan_tuples(node, rtfuncdata, rtoffset);
+ }
else
tupdesc = RelationGetDescr(dmstate->rel);
@@ -3054,7 +3210,8 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
* We do that here, not when the plan is created, because we can't know
* what aliases ruleutils.c will assign at plan creation time.
*/
- if (list_length(fdw_private) > FdwScanPrivateRelations)
+ if (list_length(fdw_private) > FdwScanPrivateRelations &&
+ list_nth(fdw_private, FdwScanPrivateRelations) != NULL)
{
StringInfoData relations;
char *rawrelations;
@@ -3104,25 +3261,90 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
rti += rtoffset;
Assert(bms_is_member(rti, plan->fs_base_relids));
rte = rt_fetch(rti, es->rtable);
- Assert(rte->rtekind == RTE_RELATION);
- /* This logic should agree with explain.c's ExplainTargetRel */
- relname = get_rel_name(rte->relid);
- if (es->verbose)
- {
- char *namespace;
- namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
- appendStringInfo(&relations, "%s.%s",
- quote_identifier(namespace),
- quote_identifier(relname));
+ if (rte->rtekind == RTE_FUNCTION)
+ {
+ List *rtfuncdata = list_nth(fdw_private, FdwScanPrivateFunctions);
+ List *funcdata;
+ bool multi_func;
+ bool first = true;
+ ListCell *lc;
+
+ funcdata = list_nth(rtfuncdata, rti - rtoffset);
+
+ multi_func = list_length(funcdata) > 1;
+
+ if (multi_func)
+ appendStringInfoString(&relations, "ROWS FROM (");
+
+ foreach(lc, funcdata)
+ {
+ List *funcinfo;
+ Oid funcid;
+
+
+ if (!first)
+ appendStringInfoString(&relations, ", ");
+
+ funcinfo = (List *) lfirst(lc);
+
+ funcid = linitial_node(Integer, funcinfo)->ival;
+ /* Checked by function_rte_pushdown_ok() */
+ Assert(OidIsValid(funcid));
+
+ /* Match RTE_RELATION behavior */
+ relname = get_func_name(funcid);
+ if (relname == NULL)
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+ if (es->verbose)
+ {
+ char *namespace;
+ Oid nsoid = get_func_namespace(funcid);
+
+ namespace = get_namespace_name(nsoid);
+ if (namespace == NULL)
+ elog(ERROR, "cache lookup failed for namespace %u", nsoid);
+
+ appendStringInfo(&relations, "%s.%s()",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfo(&relations, "%s()", quote_identifier(relname));
+
+ first = false;
+ }
+ /* Close ROWS FROM */
+ if (multi_func)
+ appendStringInfoChar(&relations, ')');
}
else
- appendStringInfoString(&relations,
- quote_identifier(relname));
+ {
+ Assert(rte->rtekind == RTE_RELATION);
+
+ /*
+ * This logic should agree with explain.c's
+ * ExplainTargetRel
+ */
+ relname = get_rel_name(rte->relid);
+ if (es->verbose)
+ {
+ char *namespace;
+
+ namespace = get_namespace_name_or_temp(get_rel_namespace(rte->relid));
+ appendStringInfo(&relations, "%s.%s",
+ quote_identifier(namespace),
+ quote_identifier(relname));
+ }
+ else
+ appendStringInfoString(&relations,
+ quote_identifier(relname));
+ }
+
refname = (char *) list_nth(es->rtable_names, rti - 1);
if (refname == NULL)
refname = rte->eref->aliasname;
- if (strcmp(refname, relname) != 0)
+ if (relname == NULL || strcmp(refname, relname) != 0)
appendStringInfo(&relations, " %s",
quote_identifier(refname));
}
@@ -3346,7 +3568,7 @@ estimate_path_cost_size(PlannerInfo *root,
* param_join_conds might contain both clauses that are safe to send
* across, and clauses that aren't.
*/
- classifyConditions(root, foreignrel, param_join_conds,
+ classifyConditions(root, foreignrel, fpinfo, param_join_conds,
&remote_param_join_conds, &local_param_join_conds);
/* Build the list of columns to be fetched from the foreign server. */
@@ -3479,8 +3701,17 @@ estimate_path_cost_size(PlannerInfo *root,
/* For join we expect inner and outer relations set */
Assert(fpinfo->innerrel && fpinfo->outerrel);
- fpinfo_i = (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
+ /*
+ * For a FUNCTION RTE absorbed into the join, use the stub fpinfo
+ * we built in foreign_join_ok(), since the function rel itself
+ * has no fdw_private.
+ */
+ fpinfo_i = fpinfo->inner_func_fpinfo ?
+ fpinfo->inner_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->innerrel->fdw_private;
+ fpinfo_o = fpinfo->outer_func_fpinfo ?
+ fpinfo->outer_func_fpinfo :
+ (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
/* Estimate of number of rows in cross product */
nrows = fpinfo_i->rows * fpinfo_o->rows;
@@ -6619,6 +6850,205 @@ semijoin_target_ok(PlannerInfo *root, RelOptInfo *joinrel, RelOptInfo *outerrel,
return ok;
}
+/*
+ * get_base_relids
+ * Return the set of base relids referenced by a foreign scan rel.
+ *
+ * For an upper rel we use the all-query relids minus the outer joins;
+ * otherwise the rel's own relids minus the outer joins. The result matches
+ * the relids that create_foreignscan_plan() ultimately uses for
+ * ForeignScan.fs_base_relids, so it is suitable for any tagging we want to
+ * store via plan-private state.
+ */
+static Relids
+get_base_relids(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids;
+
+ if (rel->reloptkind == RELOPT_UPPER_REL)
+ relids = root->all_query_rels;
+ else
+ relids = rel->relids;
+
+ return bms_difference(relids, root->outer_join_rels);
+}
+
+/*
+ * get_min_base_rti
+ * Lowest base RT index in the foreign scan rel.
+ *
+ * After setrefs.c flattens the rtable, the scan-local indexes saved in
+ * plan-private data can be translated to estate indexes by adding
+ * (ForeignScan.fs_base_relids min - this value). Captured at plan time
+ * because create_foreignscan_plan() computes the same value internally.
+ */
+static int
+get_min_base_rti(PlannerInfo *root, RelOptInfo *rel)
+{
+ Relids relids = get_base_relids(root, rel);
+
+ return bms_next_member(relids, -1);
+}
+
+/*
+ * get_functions_data
+ * Build the per-RTE function metadata list saved as
+ * FdwScanPrivateFunctions.
+ *
+ * The result list is indexed by base RT index relative to the lowest base
+ * RT index of the scan. Each element is either NULL (for non-RTE_FUNCTION
+ * base rels in this scan) or a List of List of two Integer nodes:
+ * (funcrettype, funccollation) -- one inner list per RangeTblFunction.
+ *
+ * Only RTE_FUNCTION relids actually appearing in the foreign scan's
+ * fs_base_relids contribute; others are placeholders so that the consumer
+ * can index into the result by RTI offset.
+ */
+static List *
+get_functions_data(PlannerInfo *root, RelOptInfo *rel)
+{
+ List *rtfuncdata = NIL;
+ Relids fscan_relids = get_base_relids(root, rel);
+ int i;
+
+ for (i = 0; i < root->simple_rel_array_size; i++)
+ {
+ RangeTblEntry *rte = root->simple_rte_array[i];
+ List *funcdata = NIL;
+ ListCell *lc;
+
+ if (rte == NULL || i == 0 ||
+ !bms_is_member(i, fscan_relids) ||
+ rte->rtekind != RTE_FUNCTION)
+ {
+ rtfuncdata = lappend(rtfuncdata, NULL);
+ continue;
+ }
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ Oid funcrettype;
+ Oid funccollation;
+ TupleDesc tupdesc;
+ Oid funcid;
+
+ get_expr_result_type(rtfunc->funcexpr, &funcrettype, &tupdesc);
+
+ /*
+ * function_rte_pushdown_ok() already rejected any function that
+ * doesn't return a well-defined scalar type, so this is a mere
+ * cross-check.
+ */
+ Assert(OidIsValid(funcrettype) && funcrettype != RECORDOID);
+
+ funccollation = exprCollation(rtfunc->funcexpr);
+
+ funcid = ((FuncExpr *) rtfunc->funcexpr)->funcid;
+
+ funcdata = lappend(funcdata,
+ list_make3(makeInteger(funcid),
+ makeInteger(funcrettype),
+ makeInteger(funccollation)));
+ }
+
+ rtfuncdata = lappend(rtfuncdata, funcdata);
+ }
+
+ return rtfuncdata;
+}
+
+/*
+ * Check if a relation is a FUNCTION RTE that can be absorbed into a remote
+ * join. Every function in the RTE must
+ *
+ * - return a well-defined scalar type -- we don't ship records/composite
+ * since the remote server cannot reconstruct a column definition list
+ * and our deparser does not emit one;
+ * - have a shippable expression with no mutable subnodes -- is_foreign_expr()
+ * rejects volatile/stable functions through contain_mutable_functions(),
+ * so the IMMUTABLE-only restriction is implicit;
+ * - not contain SubPlans -- we'd otherwise need to ship sub-results to
+ * the remote, which we do not implement.
+ *
+ * WITH ORDINALITY is not supported yet.
+ */
+static bool
+function_rte_pushdown_ok(PlannerInfo *root, RelOptInfo *rel,
+ RelOptInfo *fdwrel)
+{
+ RangeTblEntry *rte;
+ ListCell *lc;
+
+ if (rel->rtekind != RTE_FUNCTION)
+ return false;
+ rte = planner_rt_fetch(rel->relid, root);
+
+ /*
+ * build_simple_rel() copies rtekind straight from the RTE, so for a base
+ * rel rel->rtekind always matches the RTE's; the check above is therefore
+ * sufficient.
+ */
+ Assert(rte->rtekind == RTE_FUNCTION);
+
+ if (rte->funcordinality)
+ return false;
+
+ /*
+ * Reject up-front any function RTE that lateral-references another
+ * relation: foreign-join push-down would need to parameterise the remote
+ * query per outer row, which we don't support, and even considering the
+ * path is expensive on the planner side. The surrounding lateral_relids
+ * check in postgresGetForeignJoinPaths() would normally bail out for the
+ * joinrel, but doing the check here avoids walking the function
+ * expression entirely.
+ *
+ * Note this rejects only lateral references to another relation at the
+ * same query level. A function argument referencing an outer query level
+ * (e.g. f(outer.col) inside a subquery) is a different case: it becomes a
+ * PARAM_EXEC Param, the function rel's lateral_relids is empty, and
+ * foreign_expr_walker() intentionally treats PARAM_EXEC as shippable. The
+ * remote query then carries a parameter placeholder, and postgres_fdw's
+ * ordinary parameter machinery re-sends it on each rescan -- i.e. it
+ * works as a normal parameterized foreign scan, which is fine.
+ */
+ if (!bms_is_empty(rel->lateral_relids))
+ return false;
+
+ Assert(list_length(rte->functions) >= 1);
+
+ foreach(lc, rte->functions)
+ {
+ RangeTblFunction *rtfunc = (RangeTblFunction *) lfirst(lc);
+ TypeFuncClass functypclass;
+ Oid funcrettype;
+ TupleDesc tupdesc;
+
+ /* Refuse to deal with strange funcexprs */
+ if (!IsA(rtfunc->funcexpr, FuncExpr))
+ return false;
+
+ if (!OidIsValid(((FuncExpr *) rtfunc->funcexpr)->funcid))
+ return false;
+
+ functypclass = get_expr_result_type(rtfunc->funcexpr,
+ &funcrettype, &tupdesc);
+ if (functypclass != TYPEFUNC_SCALAR)
+ return false;
+ if (!OidIsValid(funcrettype) ||
+ funcrettype == RECORDOID ||
+ funcrettype == VOIDOID)
+ return false;
+
+ if (contain_subplans(rtfunc->funcexpr))
+ return false;
+ if (!is_foreign_expr(root, fdwrel, fdwrel->fdw_private, (Expr *) rtfunc->funcexpr))
+ return false;
+ }
+
+ return true;
+}
+
/*
* Assess whether the join between inner and outer relations can be pushed down
* to the foreign server. As a side effect, save information we obtain in this
@@ -6634,6 +7064,8 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
PgFdwRelationInfo *fpinfo_i;
ListCell *lc;
List *joinclauses;
+ bool outer_is_function = false;
+ bool inner_is_function = false;
/*
* We support pushing down INNER, LEFT, RIGHT, FULL OUTER and SEMI joins.
@@ -6652,15 +7084,86 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
return false;
/*
- * If either of the joining relations is marked as unsafe to pushdown, the
- * join can not be pushed down.
+ * Detect mixed (foreign x function-RTE) cases. Only INNER joins are
+ * supported initially. We dispatch on rtekind here so that the same
+ * function RTE can be absorbed into joins on multiple foreign servers
+ * (each call gets its own stub fpinfo and rechecks shippability for the
+ * specific server).
*/
fpinfo = (PgFdwRelationInfo *) joinrel->fdw_private;
- fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
- fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
- if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
- !fpinfo_i || !fpinfo_i->pushdown_safe)
- return false;
+ if (jointype == JOIN_INNER && innerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private) &&
+ fpinfo_o->pushdown_safe &&
+ function_rte_pushdown_ok(root, innerrel, outerrel))
+ {
+ inner_is_function = true;
+ }
+ else if (jointype == JOIN_INNER && outerrel->rtekind == RTE_FUNCTION &&
+ (fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private) &&
+ fpinfo_i->pushdown_safe &&
+ function_rte_pushdown_ok(root, outerrel, innerrel))
+ {
+ outer_is_function = true;
+ }
+ else
+ {
+ fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ if (!fpinfo_o || !fpinfo_o->pushdown_safe ||
+ !fpinfo_i || !fpinfo_i->pushdown_safe)
+ return false;
+ }
+
+ /*
+ * If one side is a function RTE, allocate a stub fpinfo so the rest of
+ * this function and the cost estimator can treat it uniformly. We hand
+ * the stub to the joinrel's deparser via the same path the foreign side
+ * uses, but we never permanently attach it to the function rel's
+ * fdw_private (different joinrels may pair the same function RTE with
+ * different foreign servers).
+ */
+ if (inner_is_function)
+ {
+ fpinfo_i = palloc0_object(PgFdwRelationInfo);
+ fpinfo_i->pushdown_safe = true;
+ fpinfo_i->server = fpinfo_o->server;
+ fpinfo_i->shippable_extensions = fpinfo_o->shippable_extensions;
+ fpinfo_i->relation_name = psprintf("%u", innerrel->relid);
+ fpinfo_i->rows = innerrel->rows;
+ fpinfo_i->width = innerrel->reltarget->width;
+ fpinfo_i->retrieved_rows = innerrel->rows;
+ fpinfo_i->rel_startup_cost = 0;
+ fpinfo_i->rel_total_cost = 0;
+
+ /*
+ * Classify the function rel's own baserestrictinfo now, so that the
+ * local_conds check below can bail out if any of it is unshippable.
+ * We classify against the stub, not the joinrel's fpinfo, because the
+ * latter's server/shippable_extensions aren't populated until
+ * merge_fdw_options() runs further down.
+ */
+ classifyConditions(root, innerrel, fpinfo_i, innerrel->baserestrictinfo,
+ &fpinfo_i->remote_conds, &fpinfo_i->local_conds);
+ fpinfo->inner_func_fpinfo = fpinfo_i;
+ }
+ else if (outer_is_function)
+ {
+ fpinfo_o = palloc0_object(PgFdwRelationInfo);
+ fpinfo_o->pushdown_safe = true;
+ fpinfo_o->server = fpinfo_i->server;
+ fpinfo_o->shippable_extensions = fpinfo_i->shippable_extensions;
+ fpinfo_o->relation_name = psprintf("%u", outerrel->relid);
+ fpinfo_o->rows = outerrel->rows;
+ fpinfo_o->width = outerrel->reltarget->width;
+ fpinfo_o->retrieved_rows = outerrel->rows;
+ fpinfo_o->rel_startup_cost = 0;
+ fpinfo_o->rel_total_cost = 0;
+
+ /* See the comment in the inner_is_function branch above. */
+ classifyConditions(root, outerrel, fpinfo_o, outerrel->baserestrictinfo,
+ &fpinfo_o->remote_conds, &fpinfo_o->local_conds);
+ fpinfo->outer_func_fpinfo = fpinfo_o;
+ }
/*
* If joining relations have local conditions, those conditions are
@@ -6698,7 +7201,7 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
foreach(lc, extra->restrictlist)
{
RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
- bool is_remote_clause = is_foreign_expr(root, joinrel,
+ bool is_remote_clause = is_foreign_expr(root, joinrel, fpinfo,
rinfo->clause);
if (IS_OUTER_JOIN(jointype) &&
@@ -7396,7 +7899,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* If any GROUP BY expression is not shippable, then we cannot
* push down aggregation to the foreign server.
*/
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
/*
@@ -7424,7 +7927,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* Non-grouping expression we need to compute. Can we ship it
* as-is to the foreign server?
*/
- if (is_foreign_expr(root, grouped_rel, expr) &&
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr) &&
!is_foreign_param(root, grouped_rel, expr))
{
/* Yes, so add to tlist as-is; OK to suppress duplicates */
@@ -7444,7 +7947,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
* don't have to check is_foreign_param, since that certainly
* won't return true for any such expression.)
*/
- if (!is_foreign_expr(root, grouped_rel, (Expr *) aggvars))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, (Expr *) aggvars))
return false;
/*
@@ -7496,7 +7999,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
grouped_rel->relids,
NULL,
NULL);
- if (is_foreign_expr(root, grouped_rel, expr))
+ if (is_foreign_expr(root, grouped_rel, fpinfo, expr))
fpinfo->remote_conds = lappend(fpinfo->remote_conds, rinfo);
else
fpinfo->local_conds = lappend(fpinfo->local_conds, rinfo);
@@ -7533,7 +8036,7 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
*/
if (IsA(expr, Aggref))
{
- if (!is_foreign_expr(root, grouped_rel, expr))
+ if (!is_foreign_expr(root, grouped_rel, fpinfo, expr))
return false;
tlist = add_to_flat_tlist(tlist, list_make1(expr));
@@ -8046,8 +8549,8 @@ add_foreign_final_paths(PlannerInfo *root, RelOptInfo *input_rel,
* Also, the LIMIT/OFFSET cannot be pushed down, if their expressions are
* not safe to remote.
*/
- if (!is_foreign_expr(root, input_rel, (Expr *) parse->limitOffset) ||
- !is_foreign_expr(root, input_rel, (Expr *) parse->limitCount))
+ if (!is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitOffset) ||
+ !is_foreign_expr(root, input_rel, ifpinfo, (Expr *) parse->limitCount))
return;
/* Safe to push down */
@@ -8693,7 +9196,7 @@ find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
if (bms_is_subset(em->em_relids, rel->relids) &&
!bms_is_empty(em->em_relids) &&
bms_is_empty(bms_intersect(em->em_relids, fpinfo->hidden_subquery_rels)) &&
- is_foreign_expr(root, rel, em->em_expr))
+ is_foreign_expr(root, rel, fpinfo, em->em_expr))
return em;
}
@@ -8715,6 +9218,7 @@ EquivalenceMember *
find_em_for_rel_target(PlannerInfo *root, EquivalenceClass *ec,
RelOptInfo *rel)
{
+ PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
PathTarget *target = rel->reltarget;
ListCell *lc1;
int i;
@@ -8764,7 +9268,7 @@ find_em_for_rel_target(PlannerInfo *root, EquivalenceClass *ec,
continue;
/* Check that expression (including relabels!) is shippable */
- if (is_foreign_expr(root, rel, em->em_expr))
+ if (is_foreign_expr(root, rel, fpinfo, em->em_expr))
return em;
}
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index a2bb1ff352c..da7da1c2ea9 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -106,6 +106,16 @@ typedef struct PgFdwRelationInfo
/* joinclauses contains only JOIN/ON conditions for an outer join */
List *joinclauses; /* List of RestrictInfo */
+ /*
+ * If a FUNCTION RTE was absorbed into this join, these point at the stub
+ * PgFdwRelationInfo for the function side (paired with the
+ * outerrel/innerrel), so the cost estimator and deparser can find it
+ * without consulting the function rel's fdw_private. At most one of
+ * outer_func_fpinfo/inner_func_fpinfo is set.
+ */
+ struct PgFdwRelationInfo *outer_func_fpinfo;
+ struct PgFdwRelationInfo *inner_func_fpinfo;
+
/* Upper relation information */
UpperRelationKind stage;
@@ -183,11 +193,13 @@ extern char *pgfdw_application_name;
/* in deparse.c */
extern void classifyConditions(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
List *input_conds,
List **remote_conds,
List **local_conds);
extern bool is_foreign_expr(PlannerInfo *root,
RelOptInfo *baserel,
+ PgFdwRelationInfo *fpinfo,
Expr *expr);
extern bool is_foreign_param(PlannerInfo *root,
RelOptInfo *baserel,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 267d3c1a7e7..2e63281ce2d 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -790,6 +790,271 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
ALTER VIEW v4 OWNER TO regress_view_owner;
+-- ===================================================================
+-- Foreign-join with FUNCTION RTE pushdown (IMMUTABLE functions only)
+-- ===================================================================
+-- IMMUTABLE function: unnest of constant array can be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+SELECT t1.c1, t1.c3 FROM ft1 t1, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id ORDER BY t1.c1;
+
+-- IMMUTABLE function: generate_series with constant args
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4) AS g(id)
+WHERE t1.c1 = g.id ORDER BY t1.c1;
+
+-- VOLATILE function (random) must NOT be pushed down
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1 FROM ft1 t1, generate_series(1, 4 + (random() * 0)::int) AS g(id)
+WHERE t1.c1 = g.id;
+
+-- WITH ORDINALITY must NOT be pushed down (limitation of this implementation)
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, u.ord
+FROM ft1 t1, unnest(ARRAY[1, 5, 10]::int[]) WITH ORDINALITY AS u(id, ord)
+WHERE t1.c1 = u.id;
+
+-- Same function RTE joined with two different foreign servers: planner picks
+-- one absorption + a local join with the second foreign server. The fact
+-- that the function is IMMUTABLE makes this safe even if both sides chose to
+-- absorb it independently.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[1, 5, 10, 100]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id ORDER BY t1.c1;
+
+-- Cost-based selection between two foreign servers: ft1 ("S 1"."T 1") has
+-- 1000 rows, ft6 ("S 1"."T 4") has ~33 rows. The same query shape gets a
+-- different push-down target depending on a predicate that changes the
+-- effective cardinality of one side -- the function "jumps" to whichever
+-- foreign scan benefits more from being pre-filtered.
+ANALYZE ft1;
+ANALYZE ft6;
+-- No extra predicate: ft1 is the bigger side, function absorbed there.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id;
+-- Selective predicate on ft1.c3 (not in the eqclass) shrinks ft1 to a
+-- handful of remote rows; now ft6 is effectively the bigger side and the
+-- function is absorbed into ft6 instead.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, t2.c1
+FROM ft1 t1, ft6 t2, unnest(ARRAY[3, 6, 9, 12, 15, 18]::int[]) AS u(id)
+WHERE t1.c1 = u.id AND t2.c1 = u.id AND t1.c3 < '00010';
+
+-- The remaining scenarios reuse a dedicated foreign table to cover the
+-- corner cases of FUNCTION RTE push-down: function-first FROM, record
+-- return type rejection, whole-row reference, UPDATE...FROM..., and
+-- multi-function ROWS FROM.
+CREATE TABLE base_tbl_fn (a int, b int);
+INSERT INTO base_tbl_fn
+ SELECT g, g + 100 FROM generate_series(1, 30) g;
+CREATE FOREIGN TABLE remote_tbl (a int, b int)
+ SERVER loopback OPTIONS (table_name 'base_tbl_fn');
+
+-- The function RTE appears first in FROM; the foreign relation must be
+-- found by scanning fs_base_relids for an RTE_RELATION rather than
+-- using scan->fs_relid blindly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n ORDER BY r.a;
+
+-- Function RTE is restricted, shippable conditions
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > 3;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > 3 ORDER BY r.a;
+
+-- Function RTE is restricted, nonshippable conditions
+CREATE FUNCTION f_local(int) RETURNS int AS $$
+BEGIN
+RETURN $1;
+END
+$$ LANGUAGE plpgsql;
+
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and n > f_local(3);
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and n > f_local(3) ORDER BY r.a;
+
+DROP FUNCTION f_local(int);
+
+-- Check that a qual on the function RTE using a function from a shippable
+-- extension is pushed down. plpgsql (not inlined) with the column as
+-- argument (not const-folded) keeps the function call in the qual.
+CREATE FUNCTION f_ext(int) RETURNS int AS $$
+BEGIN RETURN $1; END
+$$ LANGUAGE plpgsql IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f_ext(int);
+-- Force pushdown so the fixed-code plan is deterministically a Foreign
+-- Scan; on the unfixed code the qual is classified local, the join is
+-- refused, and the plan falls back to a local join instead.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r WHERE r.a = n and f_ext(n) > 3;
+SELECT * FROM unnest(array[2, 3, 4]) n, remote_tbl r
+WHERE r.a = n and f_ext(n) > 3 ORDER BY r.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+ALTER EXTENSION postgres_fdw DROP FUNCTION f_ext(int);
+DROP FUNCTION f_ext(int);
+
+-- A function returning record (composite) is forbidden; pushing it
+-- would yield a remote "column definition list is required" error.
+-- function_rte_pushdown_ok() rejects it via TYPEFUNC_SCALAR.
+CREATE OR REPLACE FUNCTION f_ret_record() RETURNS record AS $$
+ SELECT (1, 2)::record
+$$ LANGUAGE SQL IMMUTABLE;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT s FROM remote_tbl rt, f_ret_record() AS s(a int, b int)
+WHERE s.a = rt.a;
+DROP FUNCTION f_ret_record();
+
+-- UPDATE ... FROM unnest() with a complex element type; area(box)
+-- yields the value to match. The locking machinery for the
+-- non-relation RTE generates a whole-row Var that deparseColumnRef
+-- emits as ROW(f<rti>.c1, ...).
+UPDATE remote_tbl r SET b = 999
+FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+WHERE r.a = area(t.bx);
+SELECT * FROM remote_tbl WHERE a IN (23, 24, 25) ORDER BY a;
+
+-- The same shape with CASE and RETURNING; exercises the DirectModify
+-- path and the executor's tuple-desc reconstruction.
+UPDATE remote_tbl r
+ SET b = CASE WHEN random() >= 0 THEN 5 ELSE 0 END
+ FROM unnest(array[box '((2,3),(-2,-3))']) AS t(bx)
+ WHERE r.a = area(t.bx) RETURNING a, b;
+
+-- RETURNING a whole-row reference to the function RTE. Without the
+-- per-RTE function metadata being preserved on the DirectModify path
+-- (FdwDirectModifyPrivateFunctions / FdwDirectModifyPrivateMinRTIndex),
+-- this would fail with "input of anonymous composite types is not
+-- implemented".
+UPDATE remote_tbl r SET b = 7
+ FROM unnest(array[box '((2,3),(-2,-3))'],
+ array[int '1']) AS t(bx, i)
+ WHERE r.a = area(t.bx) RETURNING a, b, t;
+
+-- ROWS FROM (...) with several functions. Force pushdown because the
+-- cost model otherwise picks a local plan.
+SET enable_hashjoin = off;
+SET enable_mergejoin = off;
+SET enable_nestloop = off;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT r.a, t.n, t.s
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Whole-row Var on the absorbed function side (e.g. via a cast to
+-- text). Exercises both deparseColumnRef whole-row branch and the
+-- get_tupdesc_for_join_scan_tuples() RTE_FUNCTION metadata path.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+SELECT t::text, r.a
+ FROM remote_tbl r, ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(11, 13)) AS t(n, s)
+ WHERE r.a = t.n ORDER BY r.a;
+
+-- Check whole-row Var represented by function on the nullable
+-- left join side
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+SELECT u, ft5.c1, ft5.c2, ft4.c1, ft4.c2 FROM ft5 LEFT JOIN
+(ft4 JOIN unnest(array[1,2], array[3,4]) as u (u1, u2) ON ft4.c1=u1)
+ON ft5.c1 = ft4.c1 WHERE ft5.c1 < 10 ORDER BY ft5.c1;
+
+-- Same nullable-side whole-row Var, but with outer rows that both match
+-- and miss the (foreign x function) inner join. A multi-column function
+-- RTE is used so that the reference is a genuine whole-row Var (a single
+-- scalar-returning function would collapse to its lone column). This
+-- exercises the THEN ROW(...) side of deparseColumnRef()'s whole-row CASE
+-- (matched rows reconstruct the composite) as well as the NULL side
+-- (missed rows), confirming the executor rebuilds the anonymous composite
+-- correctly.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+SELECT l.a, t FROM remote_tbl l
+ LEFT JOIN (remote_tbl r JOIN unnest(array[2, 4], array[20, 40]) AS t(x, y)
+ ON r.a = t.x)
+ ON l.a = r.a
+ WHERE l.a < 6 ORDER BY l.a;
+RESET enable_hashjoin;
+RESET enable_mergejoin;
+RESET enable_nestloop;
+
+-- Check foreign join with parameterized function
+CREATE OR REPLACE FUNCTION f(int) RETURNS SETOF int LANGUAGE plpgsql ROWS 10 AS 'BEGIN RETURN QUERY SELECT generate_series(1,$1) ; END' IMMUTABLE;
+ALTER EXTENSION postgres_fdw ADD FUNCTION f(INTEGER);
+
+EXPLAIN (VERBOSE, COSTS OFF)
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+WITH s AS MATERIALIZED (SELECT r1.* FROM remote_tbl r1
+JOIN LATERAL
+(SELECT r2.a FROM remote_tbl r2, f(r1.a) LIMIT 1) s
+ON true)
+SELECT * FROM s ORDER BY 1;
+
+ALTER EXTENSION postgres_fdw DROP FUNCTION f(INTEGER);
+DROP FUNCTION f(INTEGER);
+
+-- Volatile function in ROWS FROM disqualifies the whole RTE.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r,
+ ROWS FROM (unnest(array[3, 6, 9]),
+ generate_series(1, 4 + (random() * 0)::int)) AS t(n, s)
+ WHERE r.a = t.n;
+
+-- LATERAL function referencing a foreign Var: postgres_fdw's
+-- foreign-join pushdown rejects this via the joinrel->lateral_relids
+-- check; the plan is therefore a local NestLoop + FunctionScan.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.x FROM remote_tbl r, LATERAL unnest(array[r.a]) AS t(x)
+WHERE r.a <= 3 ORDER BY r.a;
+
+-- Outer joins with the function RTE are not pushed down (only INNER
+-- joins are supported by function_rte_pushdown_ok()).
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a, t.n FROM remote_tbl r
+ LEFT JOIN unnest(array[1, 2, 3]) AS t(n) ON r.a = t.n
+ WHERE r.a <= 5 ORDER BY r.a;
+
+-- SEMI join (EXISTS) with a function RTE is also not pushed down.
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT r.a FROM remote_tbl r
+ WHERE EXISTS (SELECT 1 FROM unnest(array[3, 6, 9]) AS t(n) WHERE t.n = r.a)
+ ORDER BY r.a;
+
+DROP FOREIGN TABLE remote_tbl;
+DROP TABLE base_tbl_fn;
+
-- ====================================================================
-- Check that userid to use when querying the remote table is correctly
-- propagated into foreign rels present in subqueries under an UNION ALL
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..f21ae1baeb2 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -746,6 +746,30 @@ set_foreign_rel_properties(RelOptInfo *joinrel, RelOptInfo *outer_rel,
joinrel->fdwroutine = outer_rel->fdwroutine;
}
}
+ else if (OidIsValid(outer_rel->serverid) &&
+ inner_rel->rtekind == RTE_FUNCTION)
+ {
+ /*
+ * One side is a foreign relation, the other side is a function RTE.
+ * If the function is IMMUTABLE, the FDW can absorb the function call
+ * into the remote query (the result is identical regardless of which
+ * server evaluates it). Let the FDW decide whether the join is
+ * actually shippable; here we just propagate the FDW routine so the
+ * FDW gets a chance.
+ */
+ joinrel->serverid = outer_rel->serverid;
+ joinrel->userid = outer_rel->userid;
+ joinrel->useridiscurrent = outer_rel->useridiscurrent;
+ joinrel->fdwroutine = outer_rel->fdwroutine;
+ }
+ else if (OidIsValid(inner_rel->serverid) &&
+ outer_rel->rtekind == RTE_FUNCTION)
+ {
+ joinrel->serverid = inner_rel->serverid;
+ joinrel->userid = inner_rel->userid;
+ joinrel->useridiscurrent = inner_rel->useridiscurrent;
+ joinrel->fdwroutine = inner_rel->fdwroutine;
+ }
}
/*
--
2.43.0
^ permalink raw reply [nested|flat] 14+ messages in thread
end of thread, other threads:[~2026-07-07 10:25 UTC | newest]
Thread overview: 14+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2020-11-11 12:51 [PATCH v5 1/2] In-place table persistence change Kyotaro Horiguchi <[email protected]>
2021-05-20 17:43 Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2021-06-15 13:15 ` Re: Function scan FDW pushdown Ashutosh Bapat <[email protected]>
2021-10-04 07:42 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2024-11-05 16:11 ` Re: Function scan FDW pushdown [email protected]
2025-08-05 21:32 ` Re: Function scan FDW pushdown Álvaro Herrera <[email protected]>
2026-05-18 10:34 Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-18 20:06 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 13:00 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-19 15:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-05-19 18:21 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-05-20 10:17 ` Re: Function scan FDW pushdown Alexander Pyhalov <[email protected]>
2026-07-06 14:11 ` Re: Function scan FDW pushdown Alexander Korotkov <[email protected]>
2026-07-07 10:25 ` Re: Function scan FDW pushdown Alexander Pyhalov <[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