Commit 21734d2f authored by Tom Lane's avatar Tom Lane

Support writable foreign tables.

This patch adds the core-system infrastructure needed to support updates
on foreign tables, and extends contrib/postgres_fdw to allow updates
against remote Postgres servers.  There's still a great deal of room for
improvement in optimization of remote updates, but at least there's basic
functionality there now.

KaiGai Kohei, reviewed by Alexander Korotkov and Laurenz Albe, and rather
heavily revised by Tom Lane.
parent 7f49a67f
......@@ -118,7 +118,6 @@ SELECT tableoid::regclass, b FROM agg_csv;
INSERT INTO agg_csv VALUES(1,2.0);
UPDATE agg_csv SET a = 1;
DELETE FROM agg_csv WHERE a = 100;
SELECT * FROM agg_csv FOR UPDATE OF agg_csv;
-- but this should be ignored
SELECT * FROM agg_csv FOR UPDATE;
......
......@@ -185,15 +185,11 @@ SELECT tableoid::regclass, b FROM agg_csv;
-- updates aren't supported
INSERT INTO agg_csv VALUES(1,2.0);
ERROR: cannot change foreign table "agg_csv"
ERROR: cannot insert into foreign table "agg_csv"
UPDATE agg_csv SET a = 1;
ERROR: cannot change foreign table "agg_csv"
ERROR: cannot update foreign table "agg_csv"
DELETE FROM agg_csv WHERE a = 100;
ERROR: cannot change foreign table "agg_csv"
SELECT * FROM agg_csv FOR UPDATE OF agg_csv;
ERROR: row-level locks cannot be used with foreign table "agg_csv"
LINE 1: SELECT * FROM agg_csv FOR UPDATE OF agg_csv;
^
ERROR: cannot delete from foreign table "agg_csv"
-- but this should be ignored
SELECT * FROM agg_csv FOR UPDATE;
a | b
......
......@@ -47,6 +47,8 @@ typedef struct ConnCacheEntry
PGconn *conn; /* connection to foreign server, or NULL */
int xact_depth; /* 0 = no xact open, 1 = main xact open, 2 =
* one level of subxact open, etc */
bool have_prep_stmt; /* have we prepared any stmts in this xact? */
bool have_error; /* have any subxacts aborted in this xact? */
} ConnCacheEntry;
/*
......@@ -54,8 +56,9 @@ typedef struct ConnCacheEntry
*/
static HTAB *ConnectionHash = NULL;
/* for assigning cursor numbers */
/* for assigning cursor numbers and prepared statement numbers */
static unsigned int cursor_number = 0;
static unsigned int prep_stmt_number = 0;
/* tracks whether any work is needed in callback functions */
static bool xact_got_connection = false;
......@@ -78,6 +81,10 @@ static void pgfdw_subxact_callback(SubXactEvent event,
* if we don't already have a suitable one, and a transaction is opened at
* the right subtransaction nesting depth if we didn't do that already.
*
* will_prep_stmt must be true if caller intends to create any prepared
* statements. Since those don't go away automatically at transaction end
* (not even on error), we need this flag to cue manual cleanup.
*
* XXX Note that caching connections theoretically requires a mechanism to
* detect change of FDW objects to invalidate already established connections.
* We could manage that by watching for invalidation events on the relevant
......@@ -86,7 +93,8 @@ static void pgfdw_subxact_callback(SubXactEvent event,
* mid-transaction anyway.
*/
PGconn *
GetConnection(ForeignServer *server, UserMapping *user)
GetConnection(ForeignServer *server, UserMapping *user,
bool will_prep_stmt)
{
bool found;
ConnCacheEntry *entry;
......@@ -131,6 +139,8 @@ GetConnection(ForeignServer *server, UserMapping *user)
/* initialize new hashtable entry (key is already filled in) */
entry->conn = NULL;
entry->xact_depth = 0;
entry->have_prep_stmt = false;
entry->have_error = false;
}
/*
......@@ -147,6 +157,8 @@ GetConnection(ForeignServer *server, UserMapping *user)
if (entry->conn == NULL)
{
entry->xact_depth = 0; /* just to be sure */
entry->have_prep_stmt = false;
entry->have_error = false;
entry->conn = connect_pg_server(server, user);
elog(DEBUG3, "new postgres_fdw connection %p for server \"%s\"",
entry->conn, server->servername);
......@@ -157,6 +169,9 @@ GetConnection(ForeignServer *server, UserMapping *user)
*/
begin_remote_xact(entry);
/* Remember if caller will prepare statements */
entry->have_prep_stmt |= will_prep_stmt;
return entry->conn;
}
......@@ -393,6 +408,20 @@ GetCursorNumber(PGconn *conn)
return ++cursor_number;
}
/*
* Assign a "unique" number for a prepared statement.
*
* This works much like GetCursorNumber, except that we never reset the counter
* within a session. That's because we can't be 100% sure we've gotten rid
* of all prepared statements on all connections, and it's not really worth
* increasing the risk of prepared-statement name collisions by resetting.
*/
unsigned int
GetPrepStmtNumber(PGconn *conn)
{
return ++prep_stmt_number;
}
/*
* Report an error we got from the remote server.
*
......@@ -400,6 +429,10 @@ GetCursorNumber(PGconn *conn)
* res: PGresult containing the error
* clear: if true, PQclear the result (otherwise caller will handle it)
* sql: NULL, or text of remote command we tried to execute
*
* Note: callers that choose not to throw ERROR for a remote error are
* responsible for making sure that the associated ConnCacheEntry gets
* marked with have_error = true.
*/
void
pgfdw_report_error(int elevel, PGresult *res, bool clear, const char *sql)
......@@ -480,6 +513,22 @@ pgfdw_xact_callback(XactEvent event, void *arg)
if (PQresultStatus(res) != PGRES_COMMAND_OK)
pgfdw_report_error(ERROR, res, true, "COMMIT TRANSACTION");
PQclear(res);
/*
* If there were any errors in subtransactions, and we made
* prepared statements, do a DEALLOCATE ALL to make sure we
* get rid of all prepared statements. This is annoying and
* not terribly bulletproof, but it's probably not worth
* trying harder. We intentionally ignore any errors in the
* DEALLOCATE.
*/
if (entry->have_prep_stmt && entry->have_error)
{
res = PQexec(entry->conn, "DEALLOCATE ALL");
PQclear(res);
}
entry->have_prep_stmt = false;
entry->have_error = false;
break;
case XACT_EVENT_PRE_PREPARE:
......@@ -502,6 +551,8 @@ pgfdw_xact_callback(XactEvent event, void *arg)
elog(ERROR, "missed cleaning up connection during pre-commit");
break;
case XACT_EVENT_ABORT:
/* Assume we might have lost track of prepared statements */
entry->have_error = true;
/* If we're aborting, abort all remote transactions too */
res = PQexec(entry->conn, "ABORT TRANSACTION");
/* Note: can't throw ERROR, it would be infinite loop */
......@@ -509,7 +560,17 @@ pgfdw_xact_callback(XactEvent event, void *arg)
pgfdw_report_error(WARNING, res, true,
"ABORT TRANSACTION");
else
{
PQclear(res);
/* As above, make sure we've cleared any prepared stmts */
if (entry->have_prep_stmt && entry->have_error)
{
res = PQexec(entry->conn, "DEALLOCATE ALL");
PQclear(res);
}
entry->have_prep_stmt = false;
entry->have_error = false;
}
break;
}
......@@ -593,6 +654,8 @@ pgfdw_subxact_callback(SubXactEvent event, SubTransactionId mySubid,
}
else
{
/* Assume we might have lost track of prepared statements */
entry->have_error = true;
/* Rollback all remote subtransactions during abort */
snprintf(sql, sizeof(sql),
"ROLLBACK TO SAVEPOINT s%d; RELEASE SAVEPOINT s%d",
......
This diff is collapsed.
This diff is collapsed.
......@@ -21,9 +21,11 @@
#include "libpq-fe.h"
/* in connection.c */
extern PGconn *GetConnection(ForeignServer *server, UserMapping *user);
extern PGconn *GetConnection(ForeignServer *server, UserMapping *user,
bool will_prep_stmt);
extern void ReleaseConnection(PGconn *conn);
extern unsigned int GetCursorNumber(PGconn *conn);
extern unsigned int GetPrepStmtNumber(PGconn *conn);
extern void pgfdw_report_error(int elevel, PGresult *res, bool clear,
const char *sql);
......@@ -39,14 +41,20 @@ extern void classifyConditions(PlannerInfo *root,
List **param_conds,
List **local_conds,
List **param_numbers);
extern void deparseSimpleSql(StringInfo buf,
extern void deparseSelectSql(StringInfo buf,
PlannerInfo *root,
RelOptInfo *baserel,
List *local_conds);
Bitmapset *attrs_used);
extern void appendWhereClause(StringInfo buf,
bool has_where,
PlannerInfo *root,
List *exprs,
PlannerInfo *root);
bool is_first);
extern void deparseInsertSql(StringInfo buf, PlannerInfo *root, Index rtindex,
List *targetAttrs, List *returningList);
extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root, Index rtindex,
List *targetAttrs, List *returningList);
extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root, Index rtindex,
List *returningList);
extern void deparseAnalyzeSizeSql(StringInfo buf, Relation rel);
extern void deparseAnalyzeSql(StringInfo buf, Relation rel);
......
......@@ -273,3 +273,77 @@ ROLLBACK TO s;
FETCH c;
SELECT * FROM ft1 ORDER BY c1 LIMIT 1;
COMMIT;
-- ===================================================================
-- test writable foreign table stuff
-- ===================================================================
EXPLAIN (verbose, costs off)
INSERT INTO ft2 (c1,c2,c3) SELECT c1+1000,c2+100, c3 || c3 FROM ft2 LIMIT 20;
INSERT INTO ft2 (c1,c2,c3) SELECT c1+1000,c2+100, c3 || c3 FROM ft2 LIMIT 20;
INSERT INTO ft2 (c1,c2,c3)
VALUES (1101,201,'aaa'), (1102,202,'bbb'), (1103,203,'ccc') RETURNING *;
INSERT INTO ft2 (c1,c2,c3) VALUES (1104,204,'ddd'), (1105,205,'eee');
UPDATE ft2 SET c2 = c2 + 300, c3 = c3 || '_update3' WHERE c1 % 10 = 3;
UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
EXPLAIN (verbose, costs off)
UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9'
FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9'
FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING *;
EXPLAIN (verbose, costs off)
DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
-- Test that defaults and triggers on remote table work as expected
ALTER TABLE "S 1"."T 1" ALTER c6 SET DEFAULT '(^-^;)';
CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
BEGIN
NEW.c3 = NEW.c3 || '_trig_update';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER t1_br_insert BEFORE INSERT OR UPDATE
ON "S 1"."T 1" FOR EACH ROW EXECUTE PROCEDURE "S 1".F_BRTRIG();
INSERT INTO ft2 (c1,c2,c3) VALUES (1208, 218, 'fff') RETURNING *;
INSERT INTO ft2 (c1,c2,c3,c6) VALUES (1218, 218, 'ggg', '(--;') RETURNING *;
UPDATE ft2 SET c2 = c2 + 600 WHERE c1 % 10 = 8 RETURNING *;
-- Test errors thrown on remote side during update
ALTER TABLE "S 1"."T 1" ADD CONSTRAINT c2positive CHECK (c2 >= 0);
INSERT INTO ft1(c1, c2) VALUES(11, 12); -- duplicate key
INSERT INTO ft1(c1, c2) VALUES(1111, -2); -- c2positive
UPDATE ft1 SET c2 = -c2 WHERE c1 = 1; -- c2positive
-- Test savepoint/rollback behavior
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
select c2, count(*) from "S 1"."T 1" where c2 < 500 group by 1 order by 1;
begin;
update ft2 set c2 = 42 where c2 = 0;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
savepoint s1;
update ft2 set c2 = 44 where c2 = 4;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
release savepoint s1;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
savepoint s2;
update ft2 set c2 = 46 where c2 = 6;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
rollback to savepoint s2;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
release savepoint s2;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
savepoint s3;
update ft2 set c2 = -2 where c2 = 42; -- fail on remote side
rollback to savepoint s3;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
release savepoint s3;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
-- none of the above is committed yet remotely
select c2, count(*) from "S 1"."T 1" where c2 < 500 group by 1 order by 1;
commit;
select c2, count(*) from ft2 where c2 < 500 group by 1 order by 1;
select c2, count(*) from "S 1"."T 1" where c2 < 500 group by 1 order by 1;
......@@ -3040,36 +3040,41 @@ ANALYZE measurement;
Foreign data is accessed with help from a
<firstterm>foreign data wrapper</firstterm>. A foreign data wrapper is a
library that can communicate with an external data source, hiding the
details of connecting to the data source and fetching data from it. There
is a foreign data wrapper available as a <filename>contrib</> module,
which can read plain data files residing on the server. Other kind of
foreign data wrappers might be found as third party products. If none of
the existing foreign data wrappers suit your needs, you can write your
own; see <xref linkend="fdwhandler">.
details of connecting to the data source and obtaining data from it.
There are some foreign data wrappers available as <filename>contrib</>
modules; see <xref linkend="contrib">. Other kinds of foreign data
wrappers might be found as third party products. If none of the existing
foreign data wrappers suit your needs, you can write your own; see <xref
linkend="fdwhandler">.
</para>
<para>
To access foreign data, you need to create a <firstterm>foreign server</>
object, which defines how to connect to a particular external data source,
according to the set of options used by a particular foreign data
object, which defines how to connect to a particular external data source
according to the set of options used by its supporting foreign data
wrapper. Then you need to create one or more <firstterm>foreign
tables</firstterm>, which define the structure of the remote data. A
foreign table can be used in queries just like a normal table, but a
foreign table has no storage in the PostgreSQL server. Whenever it is
used, <productname>PostgreSQL</productname> asks the foreign data wrapper
to fetch the data from the external source.
to fetch data from the external source, or transmit data to the external
source in the case of update commands.
</para>
<para>
Accessing remote data may require authentication at the external
Accessing remote data may require authenticating to the external
data source. This information can be provided by a
<firstterm>user mapping</>, which can provide additional options based
<firstterm>user mapping</>, which can provide additional data
such as user names and passwords based
on the current <productname>PostgreSQL</productname> role.
</para>
<para>
Currently, foreign tables are read-only. This limitation may be fixed
in a future release.
For additional information, see
<xref linkend="sql-createforeigndatawrapper">,
<xref linkend="sql-createserver">,
<xref linkend="sql-createusermapping">, and
<xref linkend="sql-createforeigntable">.
</para>
</sect1>
......
This diff is collapsed.
......@@ -13,6 +13,7 @@
files in the server's file system. Data files must be in a format
that can be read by <command>COPY FROM</command>;
see <xref linkend="sql-copy"> for details.
Access to such data files is currently read-only.
</para>
<para>
......@@ -160,7 +161,7 @@
<example>
<title id="csvlog-fdw">Create a Foreign Table for PostgreSQL CSV Logs</title>
<para>
One of the obvious uses for the <literal>file_fdw</> is to make
the PostgreSQL activity log available as a table for querying. To
......@@ -217,8 +218,8 @@ OPTIONS ( filename '/home/josh/9.1/data/pg_log/pglog.csv', format 'csv' );
</para>
<para>
That's it &mdash; now you can query your log directly. In production, of course,
you would need to define some way to adjust to log rotation.
That's it &mdash; now you can query your log directly. In production, of
course, you would need to define some way to deal with log rotation.
</para>
</example>
......
......@@ -61,7 +61,10 @@
<para>
Now you need only <command>SELECT</> from a foreign table to access
the data stored in its underlying remote table.
the data stored in its underlying remote table. You can also modify
the remote table using <command>INSERT</>, <command>UPDATE</>, or
<command>DELETE</>. (Of course, the remote user you have specified
in your user mapping must have privileges to do these things.)
</para>
<para>
......
......@@ -117,9 +117,10 @@ CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable>
<title>Notes</title>
<para>
At the moment, the foreign-data wrapper functionality is rudimentary.
There is no support for updating a foreign table, and optimization of
queries is primitive (and mostly left to the wrapper, too).
<productname>PostgreSQL</>'s foreign-data functionality is still under
active development. Optimization of queries is primitive (and mostly left
to the wrapper, too). Thus, there is considerable room for future
performance improvements.
</para>
</refsect1>
......@@ -158,7 +159,7 @@ CREATE FOREIGN DATA WRAPPER mywrapper
9075-9 (SQL/MED), with the exception that the <literal>HANDLER</literal>
and <literal>VALIDATOR</literal> clauses are extensions and the standard
clauses <literal>LIBRARY</literal> and <literal>LANGUAGE</literal>
are not implemented in PostgreSQL.
are not implemented in <productname>PostgreSQL</>.
</para>
<para>
......@@ -175,6 +176,7 @@ CREATE FOREIGN DATA WRAPPER mywrapper
<member><xref linkend="sql-dropforeigndatawrapper"></member>
<member><xref linkend="sql-createserver"></member>
<member><xref linkend="sql-createusermapping"></member>
<member><xref linkend="sql-createforeigntable"></member>
</simplelist>
</refsect1>
......
......@@ -2121,17 +2121,10 @@ CopyFrom(CopyState cstate)
* here that basically duplicated execUtils.c ...)
*/
resultRelInfo = makeNode(ResultRelInfo);
resultRelInfo->ri_RangeTableIndex = 1; /* dummy */
resultRelInfo->ri_RelationDesc = cstate->rel;
resultRelInfo->ri_TrigDesc = CopyTriggerDesc(cstate->rel->trigdesc);
if (resultRelInfo->ri_TrigDesc)
{
resultRelInfo->ri_TrigFunctions = (FmgrInfo *)
palloc0(resultRelInfo->ri_TrigDesc->numtriggers * sizeof(FmgrInfo));
resultRelInfo->ri_TrigWhenExprs = (List **)
palloc0(resultRelInfo->ri_TrigDesc->numtriggers * sizeof(List *));
}
resultRelInfo->ri_TrigInstrument = NULL;
InitResultRelInfo(resultRelInfo,
cstate->rel,
1, /* dummy rangetable index */
0);
ExecOpenIndices(resultRelInfo);
......
......@@ -90,6 +90,7 @@ static void ExplainIndexScanDetails(Oid indexid, ScanDirection indexorderdir,
static void ExplainScanTarget(Scan *plan, ExplainState *es);
static void ExplainModifyTarget(ModifyTable *plan, ExplainState *es);
static void ExplainTargetRel(Plan *plan, Index rti, ExplainState *es);
static void show_modifytable_info(ModifyTableState *mtstate, ExplainState *es);
static void ExplainMemberNodes(List *plans, PlanState **planstates,
List *ancestors, ExplainState *es);
static void ExplainSubPlans(List *plans, List *ancestors,
......@@ -1341,6 +1342,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
show_instrumentation_count("Rows Removed by Filter", 1,
planstate, es);
break;
case T_ModifyTable:
show_modifytable_info((ModifyTableState *) planstate, es);
break;
case T_Hash:
show_hash_info((HashState *) planstate, es);
break;
......@@ -1840,7 +1844,8 @@ show_foreignscan_info(ForeignScanState *fsstate, ExplainState *es)
FdwRoutine *fdwroutine = fsstate->fdwroutine;
/* Let the FDW emit whatever fields it wants */
fdwroutine->ExplainForeignScan(fsstate, es);
if (fdwroutine->ExplainForeignScan != NULL)
fdwroutine->ExplainForeignScan(fsstate, es);
}
/*
......@@ -2036,6 +2041,34 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es)
}
}
/*
* Show extra information for a ModifyTable node
*/
static void
show_modifytable_info(ModifyTableState *mtstate, ExplainState *es)
{
FdwRoutine *fdwroutine = mtstate->resultRelInfo->ri_FdwRoutine;
/*
* If the first target relation is a foreign table, call its FDW to
* display whatever additional fields it wants to. For now, we ignore the
* possibility of other targets being foreign tables, although the API for
* ExplainForeignModify is designed to allow them to be processed.
*/
if (fdwroutine != NULL &&
fdwroutine->ExplainForeignModify != NULL)
{
ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
List *fdw_private = (List *) linitial(node->fdwPrivLists);
fdwroutine->ExplainForeignModify(mtstate,
mtstate->resultRelInfo,
fdw_private,
0,
es);
}
}
/*
* Explain the constituent plans of a ModifyTable, Append, MergeAppend,
* BitmapAnd, or BitmapOr node.
......
......@@ -44,6 +44,7 @@
#include "catalog/namespace.h"
#include "commands/trigger.h"
#include "executor/execdebug.h"
#include "foreign/fdwapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "optimizer/clauses.h"
......@@ -1005,6 +1006,7 @@ void
CheckValidResultRel(Relation resultRel, CmdType operation)
{
TriggerDesc *trigDesc = resultRel->trigdesc;
FdwRoutine *fdwroutine;
switch (resultRel->rd_rel->relkind)
{
......@@ -1069,10 +1071,35 @@ CheckValidResultRel(Relation resultRel, CmdType operation)
RelationGetRelationName(resultRel))));
break;
case RELKIND_FOREIGN_TABLE:
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot change foreign table \"%s\"",
RelationGetRelationName(resultRel))));
/* Okay only if the FDW supports it */
fdwroutine = GetFdwRoutineForRelation(resultRel, false);
switch (operation)
{
case CMD_INSERT:
if (fdwroutine->ExecForeignInsert == NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot insert into foreign table \"%s\"",
RelationGetRelationName(resultRel))));
break;
case CMD_UPDATE:
if (fdwroutine->ExecForeignUpdate == NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot update foreign table \"%s\"",
RelationGetRelationName(resultRel))));
break;
case CMD_DELETE:
if (fdwroutine->ExecForeignDelete == NULL)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot delete from foreign table \"%s\"",
RelationGetRelationName(resultRel))));
break;
default:
elog(ERROR, "unrecognized CmdType: %d", (int) operation);
break;
}
break;
default:
ereport(ERROR,
......@@ -1126,7 +1153,7 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType)
RelationGetRelationName(rel))));
break;
case RELKIND_FOREIGN_TABLE:
/* Perhaps we can support this someday, but not today */
/* Should not get here */
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot lock rows in foreign table \"%s\"",
......@@ -1180,6 +1207,11 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_TrigWhenExprs = NULL;
resultRelInfo->ri_TrigInstrument = NULL;
}
if (resultRelationDesc->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
resultRelInfo->ri_FdwRoutine = GetFdwRoutineForRelation(resultRelationDesc, true);
else
resultRelInfo->ri_FdwRoutine = NULL;
resultRelInfo->ri_FdwState = NULL;
resultRelInfo->ri_ConstraintExprs = NULL;
resultRelInfo->ri_junkFilter = NULL;
resultRelInfo->ri_projectReturning = NULL;
......
......@@ -147,7 +147,8 @@ ExecInitForeignScan(ForeignScan *node, EState *estate, int eflags)
scanstate->ss.ss_currentRelation = currentRelation;
/*
* get the scan type from the relation descriptor.
* get the scan type from the relation descriptor. (XXX at some point we
* might want to let the FDW editorialize on the scan tupdesc.)
*/
ExecAssignScanType(&scanstate->ss, RelationGetDescr(currentRelation));
......
......@@ -42,6 +42,7 @@
#include "commands/trigger.h"
#include "executor/executor.h"
#include "executor/nodeModifyTable.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "storage/bufmgr.h"
......@@ -225,6 +226,24 @@ ExecInsert(TupleTableSlot *slot,
newId = InvalidOid;
}
else if (resultRelInfo->ri_FdwRoutine)
{
/*
* insert into foreign table: let the FDW do it
*/
slot = resultRelInfo->ri_FdwRoutine->ExecForeignInsert(estate,
resultRelInfo,
slot,
planSlot);
if (slot == NULL) /* "do nothing" */
return NULL;
/* FDW might have changed tuple */
tuple = ExecMaterializeSlot(slot);
newId = InvalidOid;
}
else
{
/*
......@@ -279,7 +298,9 @@ ExecInsert(TupleTableSlot *slot,
* When deleting from a table, tupleid identifies the tuple to
* delete and oldtuple is NULL. When deleting from a view,
* oldtuple is passed to the INSTEAD OF triggers and identifies
* what to delete, and tupleid is invalid.
* what to delete, and tupleid is invalid. When deleting from a
* foreign table, both tupleid and oldtuple are NULL; the FDW has
* to figure out which row to delete using data from the planSlot.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
......@@ -296,6 +317,7 @@ ExecDelete(ItemPointer tupleid,
Relation resultRelationDesc;
HTSU_Result result;
HeapUpdateFailureData hufd;
TupleTableSlot *slot = NULL;
/*
* get information on the (current) result relation
......@@ -334,6 +356,27 @@ ExecDelete(ItemPointer tupleid,
if (!dodelete) /* "do nothing" */
return NULL;
}
else if (resultRelInfo->ri_FdwRoutine)
{
/*
* delete from foreign table: let the FDW do it
*
* We offer the trigger tuple slot as a place to store RETURNING data,
* although the FDW can return some other slot if it wants. Set up
* the slot's tupdesc so the FDW doesn't need to do that for itself.
*/
slot = estate->es_trig_tuple_slot;
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
slot = resultRelInfo->ri_FdwRoutine->ExecForeignDelete(estate,
resultRelInfo,
slot,
planSlot);
if (slot == NULL) /* "do nothing" */
return NULL;
}
else
{
/*
......@@ -443,34 +486,49 @@ ldelete:;
* We have to put the target tuple into a slot, which means first we
* gotta fetch it. We can use the trigger tuple slot.
*/
TupleTableSlot *slot = estate->es_trig_tuple_slot;
TupleTableSlot *rslot;
HeapTupleData deltuple;
Buffer delbuffer;
if (oldtuple != NULL)
if (resultRelInfo->ri_FdwRoutine)
{
deltuple.t_data = oldtuple;
deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
ItemPointerSetInvalid(&(deltuple.t_self));
deltuple.t_tableOid = InvalidOid;
/* FDW must have provided a slot containing the deleted row */
Assert(!TupIsNull(slot));
delbuffer = InvalidBuffer;
}
else
{
deltuple.t_self = *tupleid;
if (!heap_fetch(resultRelationDesc, SnapshotAny,
&deltuple, &delbuffer, false, NULL))
elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
}
slot = estate->es_trig_tuple_slot;
if (oldtuple != NULL)
{
deltuple.t_data = oldtuple;
deltuple.t_len = HeapTupleHeaderGetDatumLength(oldtuple);
ItemPointerSetInvalid(&(deltuple.t_self));
deltuple.t_tableOid = InvalidOid;
delbuffer = InvalidBuffer;
}
else
{
deltuple.t_self = *tupleid;
if (!heap_fetch(resultRelationDesc, SnapshotAny,
&deltuple, &delbuffer, false, NULL))
elog(ERROR, "failed to fetch deleted tuple for DELETE RETURNING");
}
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
ExecStoreTuple(&deltuple, slot, InvalidBuffer, false);
if (slot->tts_tupleDescriptor != RelationGetDescr(resultRelationDesc))
ExecSetSlotDescriptor(slot, RelationGetDescr(resultRelationDesc));
ExecStoreTuple(&deltuple, slot, InvalidBuffer, false);
}
rslot = ExecProcessReturning(resultRelInfo->ri_projectReturning,
slot, planSlot);
/*
* Before releasing the target tuple again, make sure rslot has a
* local copy of any pass-by-reference values.
*/
ExecMaterializeSlot(rslot);
ExecClearTuple(slot);
if (BufferIsValid(delbuffer))
ReleaseBuffer(delbuffer);
......@@ -494,7 +552,9 @@ ldelete:;
* When updating a table, tupleid identifies the tuple to
* update and oldtuple is NULL. When updating a view, oldtuple
* is passed to the INSTEAD OF triggers and identifies what to
* update, and tupleid is invalid.
* update, and tupleid is invalid. When updating a foreign table,
* both tupleid and oldtuple are NULL; the FDW has to figure out
* which row to update using data from the planSlot.
*
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
......@@ -568,6 +628,22 @@ ExecUpdate(ItemPointer tupleid,
/* trigger might have changed tuple */
tuple = ExecMaterializeSlot(slot);
}
else if (resultRelInfo->ri_FdwRoutine)
{
/*
* update in foreign table: let the FDW do it
*/
slot = resultRelInfo->ri_FdwRoutine->ExecForeignUpdate(estate,
resultRelInfo,
slot,
planSlot);
if (slot == NULL) /* "do nothing" */
return NULL;
/* FDW might have changed tuple */
tuple = ExecMaterializeSlot(slot);
}
else
{
LockTupleMode lockmode;
......@@ -867,10 +943,12 @@ ExecModifyTable(ModifyTableState *node)
*/
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
char relkind;
Datum datum;
bool isNull;
if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_RELATION)
relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
if (relkind == RELKIND_RELATION)
{
datum = ExecGetJunkAttribute(slot,
junkfilter->jf_junkAttNo,
......@@ -884,6 +962,10 @@ ExecModifyTable(ModifyTableState *node)
* ctid!! */
tupleid = &tuple_ctid;
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
/* do nothing; FDW must fetch any junk attrs it wants */
}
else
{
datum = ExecGetJunkAttribute(slot,
......@@ -1026,6 +1108,19 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
estate->es_result_relation_info = resultRelInfo;
mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
/* Also let FDWs init themselves for foreign-table result rels */
if (resultRelInfo->ri_FdwRoutine != NULL &&
resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL)
{
List *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate,
resultRelInfo,
fdw_private,
i,
eflags);
}
resultRelInfo++;
i++;
}
......@@ -1180,12 +1275,19 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
if (operation == CMD_UPDATE || operation == CMD_DELETE)
{
/* For UPDATE/DELETE, find the appropriate junk attr now */
if (resultRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_RELATION)
char relkind;
relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
if (relkind == RELKIND_RELATION)
{
j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
if (!AttributeNumberIsValid(j->jf_junkAttNo))
elog(ERROR, "could not find junk ctid column");
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
/* FDW must fetch any junk attrs it wants */
}
else
{
j->jf_junkAttNo = ExecFindJunkAttribute(j, "wholerow");
......@@ -1243,6 +1345,19 @@ ExecEndModifyTable(ModifyTableState *node)
{
int i;
/*
* Allow any FDWs to shut down
*/
for (i = 0; i < node->mt_nplans; i++)
{
ResultRelInfo *resultRelInfo = node->resultRelInfo + i;
if (resultRelInfo->ri_FdwRoutine != NULL &&
resultRelInfo->ri_FdwRoutine->EndForeignModify != NULL)
resultRelInfo->ri_FdwRoutine->EndForeignModify(node->ps.state,
resultRelInfo);
}
/*
* Free the exprcontext
*/
......
......@@ -179,6 +179,7 @@ _copyModifyTable(const ModifyTable *from)
COPY_SCALAR_FIELD(resultRelIndex);
COPY_NODE_FIELD(plans);
COPY_NODE_FIELD(returningLists);
COPY_NODE_FIELD(fdwPrivLists);
COPY_NODE_FIELD(rowMarks);
COPY_SCALAR_FIELD(epqParam);
......
......@@ -333,6 +333,7 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
WRITE_INT_FIELD(resultRelIndex);
WRITE_NODE_FIELD(plans);
WRITE_NODE_FIELD(returningLists);
WRITE_NODE_FIELD(fdwPrivLists);
WRITE_NODE_FIELD(rowMarks);
WRITE_INT_FIELD(epqParam);
}
......
......@@ -20,6 +20,7 @@
#include <math.h>
#include "access/skey.h"
#include "catalog/pg_class.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
......@@ -4695,7 +4696,8 @@ make_result(PlannerInfo *root,
* to make it look better sometime.
*/
ModifyTable *
make_modifytable(CmdType operation, bool canSetTag,
make_modifytable(PlannerInfo *root,
CmdType operation, bool canSetTag,
List *resultRelations,
List *subplans, List *returningLists,
List *rowMarks, int epqParam)
......@@ -4703,7 +4705,10 @@ make_modifytable(CmdType operation, bool canSetTag,
ModifyTable *node = makeNode(ModifyTable);
Plan *plan = &node->plan;
double total_size;
List *fdw_private_list;
ListCell *subnode;
ListCell *lc;
int i;
Assert(list_length(resultRelations) == list_length(subplans));
Assert(returningLists == NIL ||
......@@ -4746,6 +4751,53 @@ make_modifytable(CmdType operation, bool canSetTag,
node->rowMarks = rowMarks;
node->epqParam = epqParam;
/*
* For each result relation that is a foreign table, allow the FDW to
* construct private plan data, and accumulate it all into a list.
*/
fdw_private_list = NIL;
i = 0;
foreach(lc, resultRelations)
{
Index rti = lfirst_int(lc);
FdwRoutine *fdwroutine;
List *fdw_private;
/*
* If possible, we want to get the FdwRoutine from our RelOptInfo for
* the table. But sometimes we don't have a RelOptInfo and must get
* it the hard way. (In INSERT, the target relation is not scanned,
* so it's not a baserel; and there are also corner cases for
* updatable views where the target rel isn't a baserel.)
*/
if (rti < root->simple_rel_array_size &&
root->simple_rel_array[rti] != NULL)
{
RelOptInfo *resultRel = root->simple_rel_array[rti];
fdwroutine = resultRel->fdwroutine;
}
else
{
RangeTblEntry *rte = planner_rt_fetch(rti, root);
Assert(rte->rtekind == RTE_RELATION);
if (rte->relkind == RELKIND_FOREIGN_TABLE)
fdwroutine = GetFdwRoutineByRelId(rte->relid);
else
fdwroutine = NULL;
}
if (fdwroutine != NULL &&
fdwroutine->PlanForeignModify != NULL)
fdw_private = fdwroutine->PlanForeignModify(root, node, rti, i);
else
fdw_private = NIL;
fdw_private_list = lappend(fdw_private_list, fdw_private);
i++;
}
node->fdwPrivLists = fdw_private_list;
return node;
}
......
......@@ -571,7 +571,8 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
else
rowMarks = root->rowMarks;
plan = (Plan *) make_modifytable(parse->commandType,
plan = (Plan *) make_modifytable(root,
parse->commandType,
parse->canSetTag,
list_make1_int(parse->resultRelation),
list_make1(plan),
......@@ -964,7 +965,8 @@ inheritance_planner(PlannerInfo *root)
rowMarks = root->rowMarks;
/* And last, tack on a ModifyTable node to do the UPDATE/DELETE work */
return (Plan *) make_modifytable(parse->commandType,
return (Plan *) make_modifytable(root,
parse->commandType,
parse->canSetTag,
resultRelations,
subplans,
......@@ -2035,6 +2037,15 @@ preprocess_rowmarks(PlannerInfo *root)
if (rte->rtekind != RTE_RELATION)
continue;
/*
* Similarly, ignore RowMarkClauses for foreign tables; foreign tables
* will instead get ROW_MARK_COPY items in the next loop. (FDWs might
* choose to do something special while fetching their rows, but that
* is of no concern here.)
*/
if (rte->relkind == RELKIND_FOREIGN_TABLE)
continue;
rels = bms_del_member(rels, rc->rti);
newrc = makeNode(PlanRowMark);
......
......@@ -6,7 +6,8 @@
* For INSERT and UPDATE queries, the targetlist must contain an entry for
* each attribute of the target relation in the correct order. For all query
* types, we may need to add junk tlist entries for Vars used in the RETURNING
* list and row ID information needed for EvalPlanQual checking.
* list and row ID information needed for SELECT FOR UPDATE locking and/or
* EvalPlanQual checking.
*
* NOTE: the rewriter's rewriteTargetListIU and rewriteTargetListUD
* routines also do preprocessing of the targetlist. The division of labor
......
......@@ -2164,7 +2164,7 @@ transformCreateTableAsStmt(ParseState *pstate, CreateTableAsStmt *stmt)
/*
* Check for features that are not supported together with FOR [KEY] UPDATE/SHARE.
* Check for features that are not supported with FOR [KEY] UPDATE/SHARE.
*
* exported so planner can check again after rewriting, query pullup, etc
*/
......@@ -2239,9 +2239,6 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
switch (rte->rtekind)
{
case RTE_RELATION:
/* ignore foreign tables */
if (rte->relkind == RELKIND_FOREIGN_TABLE)
break;
applyLockingClause(qry, i,
lc->strength, lc->noWait, pushedDown);
rte->requiredPerms |= ACL_SELECT_FOR_UPDATE;
......@@ -2251,7 +2248,7 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
lc->strength, lc->noWait, pushedDown);
/*
* FOR [KEY] UPDATE/SHARE of subquery is propagated to all of
* FOR UPDATE/SHARE of subquery is propagated to all of
* subquery's rels, too. We could do this later (based on
* the marking of the subquery RTE) but it is convenient
* to have local knowledge in each query level about which
......@@ -2291,12 +2288,6 @@ transformLockingClause(ParseState *pstate, Query *qry, LockingClause *lc,
switch (rte->rtekind)
{
case RTE_RELATION:
if (rte->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("row-level locks cannot be used with foreign table \"%s\"",
rte->eref->aliasname),
parser_errposition(pstate, thisrel->location)));
applyLockingClause(qry, i,
lc->strength, lc->noWait,
pushedDown);
......
......@@ -16,6 +16,7 @@
#include "access/sysattr.h"
#include "catalog/pg_type.h"
#include "commands/trigger.h"
#include "foreign/fdwapi.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "parser/analyze.h"
......@@ -1156,6 +1157,7 @@ rewriteValuesRTE(RangeTblEntry *rte, Relation target_relation, List *attrnos)
* is a regular table, the junk TLE emits the ctid attribute of the original
* row. When the target relation is a view, there is no ctid, so we instead
* emit a whole-row Var that will contain the "old" values of the view row.
* If it's a foreign table, we let the FDW decide what to add.
*
* For UPDATE queries, this is applied after rewriteTargetListIU. The
* ordering isn't actually critical at the moment.
......@@ -1183,6 +1185,21 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
attrname = "ctid";
}
else if (target_relation->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
{
/*
* Let the foreign table's FDW add whatever junk TLEs it wants.
*/
FdwRoutine *fdwroutine;
fdwroutine = GetFdwRoutineForRelation(target_relation, false);
if (fdwroutine->AddForeignUpdateTargets != NULL)
fdwroutine->AddForeignUpdateTargets(parsetree, target_rte,
target_relation);
return;
}
else
{
/*
......@@ -1444,17 +1461,13 @@ markQueryForLocking(Query *qry, Node *jtnode,
if (rte->rtekind == RTE_RELATION)
{
/* ignore foreign tables */
if (rte->relkind != RELKIND_FOREIGN_TABLE)
{
applyLockingClause(qry, rti, strength, noWait, pushedDown);
rte->requiredPerms |= ACL_SELECT_FOR_UPDATE;
}
applyLockingClause(qry, rti, strength, noWait, pushedDown);
rte->requiredPerms |= ACL_SELECT_FOR_UPDATE;
}
else if (rte->rtekind == RTE_SUBQUERY)
{
applyLockingClause(qry, rti, strength, noWait, pushedDown);
/* FOR [KEY] UPDATE/SHARE of subquery is propagated to subquery's rels */
/* FOR UPDATE/SHARE of subquery is propagated to subquery's rels */
markQueryForLocking(rte->subquery, (Node *) rte->subquery->jointree,
strength, noWait, true);
}
......
This diff is collapsed.
......@@ -270,7 +270,8 @@ typedef struct ProjectionInfo
* resultSlot: tuple slot used to hold cleaned tuple.
* junkAttNo: not used by junkfilter code. Can be used by caller
* to remember the attno of a specific junk attribute
* (execMain.c stores the "ctid" attno here).
* (nodeModifyTable.c keeps the "ctid" or "wholerow"
* attno here).
* ----------------
*/
typedef struct JunkFilter
......@@ -300,6 +301,8 @@ typedef struct JunkFilter
* TrigFunctions cached lookup info for trigger functions
* TrigWhenExprs array of trigger WHEN expr states
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
* projectReturning for computing a RETURNING list
......@@ -317,6 +320,8 @@ typedef struct ResultRelInfo
FmgrInfo *ri_TrigFunctions;
List **ri_TrigWhenExprs;
Instrumentation *ri_TrigInstrument;
struct FdwRoutine *ri_FdwRoutine;
void *ri_FdwState;
List **ri_ConstraintExprs;
JunkFilter *ri_junkFilter;
ProjectionInfo *ri_projectReturning;
......
This diff is collapsed.
......@@ -79,7 +79,8 @@ extern SetOp *make_setop(SetOpCmd cmd, SetOpStrategy strategy, Plan *lefttree,
long numGroups, double outputRows);
extern Result *make_result(PlannerInfo *root, List *tlist,
Node *resconstantqual, Plan *subplan);
extern ModifyTable *make_modifytable(CmdType operation, bool canSetTag,
extern ModifyTable *make_modifytable(PlannerInfo *root,
CmdType operation, bool canSetTag,
List *resultRelations, List *subplans, List *returningLists,
List *rowMarks, int epqParam);
extern bool is_projection_capable_plan(Plan *plan);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment