diff --git a/contrib/pgsql_fdw/connection.c b/contrib/pgsql_fdw/connection.c index c971bd7..7cb448b 100644 *** a/contrib/pgsql_fdw/connection.c --- b/contrib/pgsql_fdw/connection.c *************** connect_pg_server(ForeignServer *server, *** 293,299 **** } /* ! * Start transaction to use cursor to retrieve data separately. */ static void begin_remote_tx(PGconn *conn) --- 293,299 ---- } /* ! * Start remote transaction with proper isolation level. */ static void begin_remote_tx(PGconn *conn) diff --git a/contrib/pgsql_fdw/deparse.c b/contrib/pgsql_fdw/deparse.c index 6f8f753..0f94551 100644 *** a/contrib/pgsql_fdw/deparse.c --- b/contrib/pgsql_fdw/deparse.c *************** void *** 82,89 **** deparseSimpleSql(StringInfo buf, Oid relid, PlannerInfo *root, ! RelOptInfo *baserel, ! ForeignTable *table) { StringInfoData foreign_relname; bool first; --- 82,88 ---- deparseSimpleSql(StringInfo buf, Oid relid, PlannerInfo *root, ! RelOptInfo *baserel) { StringInfoData foreign_relname; bool first; *************** deparseSimpleSql(StringInfo buf, *** 173,179 **** * deparse FROM clause, including alias if any */ appendStringInfo(buf, "FROM "); ! deparseRelation(buf, table->relid, root, true); elog(DEBUG3, "Remote SQL: %s", buf->data); } --- 172,178 ---- * deparse FROM clause, including alias if any */ appendStringInfo(buf, "FROM "); ! deparseRelation(buf, relid, root, true); elog(DEBUG3, "Remote SQL: %s", buf->data); } diff --git a/contrib/pgsql_fdw/expected/pgsql_fdw.out b/contrib/pgsql_fdw/expected/pgsql_fdw.out index 8e50614..c486094 100644 *** a/contrib/pgsql_fdw/expected/pgsql_fdw.out --- b/contrib/pgsql_fdw/expected/pgsql_fdw.out *************** ALTER SERVER loopback1 OPTIONS ( *** 113,120 **** ); ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR ERROR: invalid option "user" ! HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib, fetch_count ! ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2'); ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (DROP user, DROP password); ALTER USER MAPPING FOR public SERVER loopback1 --- 113,119 ---- ); ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR ERROR: invalid option "user" ! HINT: Valid options in this context are: authtype, service, connect_timeout, dbname, host, hostaddr, port, tty, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, requiressl, sslcompression, sslmode, sslcert, sslkey, sslrootcert, sslcrl, requirepeer, krbsrvname, gsslib ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (DROP user, DROP password); ALTER USER MAPPING FOR public SERVER loopback1 *************** ALTER USER MAPPING FOR public SERVER loo *** 122,137 **** ERROR: invalid option "host" HINT: Valid options in this context are: user, password ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1'); ! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100'); ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR ERROR: invalid option "invalid" ! HINT: Valid options in this context are: nspname, relname, fetch_count ! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR ! ERROR: invalid value for fetch_count: "a" ! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR ! ERROR: invalid value for fetch_count: "0" ! ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR ! ERROR: invalid value for fetch_count: "-1" ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR ERROR: invalid option "invalid" HINT: Valid options in this context are: colname --- 121,130 ---- ERROR: invalid option "host" HINT: Valid options in this context are: user, password ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1'); ! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1'); ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR ERROR: invalid option "invalid" ! HINT: Valid options in this context are: nspname, relname ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR ERROR: invalid option "invalid" HINT: Valid options in this context are: colname *************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 *** 149,155 **** Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------- loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') | ! loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression', fetch_count '2') | (2 rows) \deu+ --- 142,148 ---- Name | Owner | Foreign-data wrapper | Access privileges | Type | Version | FDW Options | Description -----------+----------------+----------------------+-------------------+------+---------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------- loopback1 | pgsql_fdw_user | pgsql_fdw | | | | (authtype 'value', service 'value', connect_timeout 'value', dbname 'value', host 'value', hostaddr 'value', port 'value', tty 'value', options 'value', application_name 'value', keepalives 'value', keepalives_idle 'value', keepalives_interval 'value', sslcompression 'value', sslmode 'value', sslcert 'value', sslkey 'value', sslrootcert 'value', sslcrl 'value') | ! loopback2 | pgsql_fdw_user | pgsql_fdw | | | | (dbname 'contrib_regression') | (2 rows) \deu+ *************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 *** 161,189 **** (2 rows) \det+ ! List of foreign tables ! Schema | Table | Server | FDW Options | Description ! --------+-------+-----------+---------------------------------------------------+------------- ! public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') | ! public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1', fetch_count '100') | (2 rows) -- =================================================================== -- simple queries -- =================================================================== -- single table, with/without alias ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; ! QUERY PLAN ! ------------------------------------------------------------------------------------------------------------------------------ Limit - Output: c1, c2, c3, c4, c5, c6, c7 -> Sort ! Output: c1, c2, c3, c4, c5, c6, c7 ! Sort Key: ft1.c3, ft1.c1 ! -> Foreign Scan on public.ft1 ! Output: c1, c2, c3, c4, c5, c6, c7 ! Remote SQL: DECLARE pgsql_fdw_cursor_0 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" ! (8 rows) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; c1 | c2 | c3 | c4 | c5 | c6 | c7 --- 154,179 ---- (2 rows) \det+ ! List of foreign tables ! Schema | Table | Server | FDW Options | Description ! --------+-------+-----------+--------------------------------+------------- ! public | ft1 | loopback2 | (nspname 'S 1', relname 'T 1') | ! public | ft2 | loopback2 | (nspname 'S 1', relname 'T 1') | (2 rows) -- =================================================================== -- simple queries -- =================================================================== -- single table, with/without alias ! EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; ! QUERY PLAN ! --------------------------------------------------------------------------------- Limit -> Sort ! Sort Key: c3, c1 ! -> Foreign Scan on ft1 ! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" ! (5 rows) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; c1 | c2 | c3 | c4 | c5 | c6 | c7 *************** SELECT * FROM ft1 ORDER BY c3, c1 OFFSET *** 200,217 **** 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 (10 rows) ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; ! QUERY PLAN ! ------------------------------------------------------------------------------------------------------------------------------ Limit - Output: c1, c2, c3, c4, c5, c6, c7 -> Sort ! Output: c1, c2, c3, c4, c5, c6, c7 ! Sort Key: t1.c3, t1.c1 ! -> Foreign Scan on public.ft1 t1 ! Output: c1, c2, c3, c4, c5, c6, c7 ! Remote SQL: DECLARE pgsql_fdw_cursor_2 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" ! (8 rows) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; c1 | c2 | c3 | c4 | c5 | c6 | c7 --- 190,204 ---- 110 | 0 | 00110 | Sun Jan 11 00:00:00 1970 PST | Sun Jan 11 00:00:00 1970 | 0 | 0 (10 rows) ! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; ! QUERY PLAN ! --------------------------------------------------------------------------------- Limit -> Sort ! Sort Key: c3, c1 ! -> Foreign Scan on ft1 t1 ! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" ! (5 rows) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; c1 | c2 | c3 | c4 | c5 | c6 | c7 *************** SELECT * FROM ft1 t1 ORDER BY t1.c3, t1. *** 229,242 **** (10 rows) -- with WHERE clause ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; ! QUERY PLAN ! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ! Foreign Scan on public.ft1 t1 ! Output: c1, c2, c3, c4, c5, c6, c7 ! Filter: (t1.c7 >= '1'::bpchar) ! Remote SQL: DECLARE pgsql_fdw_cursor_4 SCROLL CURSOR FOR SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text)) ! (4 rows) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; c1 | c2 | c3 | c4 | c5 | c6 | c7 --- 216,228 ---- (10 rows) -- with WHERE clause ! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; ! QUERY PLAN ! -------------------------------------------------------------------------------------------------------------------------------------------------------------------- ! Foreign Scan on ft1 t1 ! Filter: (c7 >= '1'::bpchar) ! Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7 FROM "S 1"."T 1" WHERE (("C 1" OPERATOR(pg_catalog.=) 101)) AND (((c6)::text OPERATOR(pg_catalog.=) '1'::text)) ! (3 rows) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; c1 | c2 | c3 | c4 | c5 | c6 | c7 diff --git a/contrib/pgsql_fdw/option.c b/contrib/pgsql_fdw/option.c index 542ef01..0192fd2 100644 *** a/contrib/pgsql_fdw/option.c --- b/contrib/pgsql_fdw/option.c *************** *** 26,37 **** #include "pgsql_fdw.h" /* - * Default fetch count for cursor. This can be overridden by fetch_count FDW - * option. - */ - #define DEFAULT_FETCH_COUNT 10000 - - /* * SQL functions */ extern Datum pgsql_fdw_validator(PG_FUNCTION_ARGS); --- 26,31 ---- *************** static PgsqlFdwOption valid_options[] = *** 105,117 **** {"relname", ForeignTableRelationId, false}, {"colname", AttributeRelationId, false}, - /* - * Options for cursor behavior. - * These options can be overridden by finer-grained objects. - */ - {"fetch_count", ForeignTableRelationId, false}, - {"fetch_count", ForeignServerRelationId, false}, - /* Terminating entry --- MUST BE LAST */ {NULL, InvalidOid, false} }; --- 99,104 ---- *************** pgsql_fdw_validator(PG_FUNCTION_ARGS) *** 165,184 **** errhint("Valid options in this context are: %s", buf.data))); } - - /* fetch_count be positive digit number. */ - if (strcmp(def->defname, "fetch_count") == 0) - { - long value; - char *p = NULL; - - value = strtol(defGetString(def), &p, 10); - if (*p != '\0' || value < 1) - ereport(ERROR, - (errcode(ERRCODE_FDW_INVALID_ATTRIBUTE_VALUE), - errmsg("invalid value for %s: \"%s\"", - def->defname, defGetString(def)))); - } } /* --- 152,157 ---- *************** ExtractConnectionOptions(List *defelems, *** 247,285 **** return i; } - /* - * Return fetch_count which should be used for the foreign table. - */ - int - GetFetchCountOption(ForeignTable *table, ForeignServer *server) - { - int fetch_count = DEFAULT_FETCH_COUNT; - ListCell *lc; - DefElem *def; - - /* - * Use specified fetch_count instead of default value, if any. Foreign - * table option overrides server option. - */ - foreach(lc, table->options) - { - def = (DefElem *) lfirst(lc); - if (strcmp(def->defname, "fetch_count") == 0) - break; - - } - if (lc == NULL) - { - foreach(lc, server->options) - { - def = (DefElem *) lfirst(lc); - if (strcmp(def->defname, "fetch_count") == 0) - break; - - } - } - if (lc != NULL) - fetch_count = strtol(defGetString(def), NULL, 10); - - return fetch_count; - } --- 220,222 ---- diff --git a/contrib/pgsql_fdw/pgsql_fdw.c b/contrib/pgsql_fdw/pgsql_fdw.c index 8975955..aede449 100644 *** a/contrib/pgsql_fdw/pgsql_fdw.c --- b/contrib/pgsql_fdw/pgsql_fdw.c *************** PG_MODULE_MAGIC; *** 45,59 **** */ #define TRANSFER_COSTS_PER_BYTE 0.001 - /* - * Cursors which are used together in a local query require different name, so - * we use simple incremental name for that purpose. We don't care wrap around - * of cursor_id because it's hard to imagine that 2^32 cursors are used in a - * query. - */ - #define CURSOR_NAME_FORMAT "pgsql_fdw_cursor_%u" - static uint32 cursor_id = 0; - /* Convenient macros for accessing the first record of PGresult. */ #define PGRES_VAL0(col) (PQgetvalue(res, 0, (col))) #define PGRES_NULL0(col) (PQgetisnull(res, 0, (col))) --- 45,50 ---- *************** typedef struct PgsqlFdwPlanState { *** 85,111 **** * Index of FDW-private information stored in fdw_private list. * * We store various information in ForeignScan.fdw_private to pass them beyond ! * the boundary between planner and executor. Finally FdwPlan using cursor ! * would hold items below: * * 1) plain SELECT statement - * 2) SQL statement used to declare cursor - * 3) SQL statement used to fetch rows from cursor - * 4) SQL statement used to reset cursor - * 5) SQL statement used to close cursor * * These items are indexed with the enum FdwPrivateIndex, so an item ! * can be accessed directly via list_nth(). For example of FETCH ! * statement: ! * list_nth(fdw_private, FdwPrivateFetchSql) */ enum FdwPrivateIndex { /* SQL statements */ FdwPrivateSelectSql, - FdwPrivateDeclareSql, - FdwPrivateFetchSql, - FdwPrivateResetSql, - FdwPrivateCloseSql, /* # of elements stored in the list fdw_private */ FdwPrivateNum, --- 76,93 ---- * Index of FDW-private information stored in fdw_private list. * * We store various information in ForeignScan.fdw_private to pass them beyond ! * the boundary between planner and executor. Finally FdwPlan holds items ! * below: * * 1) plain SELECT statement * * These items are indexed with the enum FdwPrivateIndex, so an item ! * can be accessed directly via list_nth(). For example of SELECT statement: ! * sql = list_nth(fdw_private, FdwPrivateSelectSql) */ enum FdwPrivateIndex { /* SQL statements */ FdwPrivateSelectSql, /* # of elements stored in the list fdw_private */ FdwPrivateNum, *************** typedef struct PgsqlFdwExecutionState *** 132,137 **** --- 114,120 ---- /* for storing result tuples */ MemoryContext scan_cxt; /* context for per-scan lifespan data */ + MemoryContext temp_cxt; /* context for per-tuple temporary data */ Tuplestorestate *tuples; /* result of the scan */ /* for error handling. */ *************** static void get_remote_estimate(const ch *** 178,185 **** static void adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost); static void execute_query(ForeignScanState *node); ! static PGresult *fetch_result(ForeignScanState *node); ! static void store_result(ForeignScanState *node, PGresult *res); static void pgsql_fdw_error_callback(void *arg); /* Exported functions, but not written in pgsql_fdw.h. */ --- 161,168 ---- static void adjust_costs(double rows, int width, Cost *startup_cost, Cost *total_cost); static void execute_query(ForeignScanState *node); ! static int query_row_processor(PGresult *res, const PGdataValue *columns, ! const char **errmsgp, void *param); static void pgsql_fdw_error_callback(void *arg); /* Exported functions, but not written in pgsql_fdw.h. */ *************** pgsqlGetForeignRelSize(PlannerInfo *root *** 288,294 **** * appended later. */ sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds); ! deparseSimpleSql(sql, foreigntableid, root, baserel, table); if (list_length(remote_conds) > 0) { appendWhereClause(sql, fpstate->has_where, remote_conds, root); --- 271,277 ---- * appended later. */ sortConditions(root, baserel, &remote_conds, ¶m_conds, &local_conds); ! deparseSimpleSql(sql, foreigntableid, root, baserel); if (list_length(remote_conds) > 0) { appendWhereClause(sql, fpstate->has_where, remote_conds, root); *************** pgsqlGetForeignPlan(PlannerInfo *root, *** 400,409 **** PgsqlFdwPlanState *fpstate = (PgsqlFdwPlanState *) baserel->fdw_private; Index scan_relid = baserel->relid; List *fdw_private = NIL; - char name[128]; /* must be larger than format + 10 */ - StringInfoData cursor; - int fetch_count; - char *sql; List *fdw_exprs = NIL; List *local_exprs = NIL; ListCell *lc; --- 383,388 ---- *************** pgsqlGetForeignPlan(PlannerInfo *root, *** 420,454 **** foreach(lc, fpstate->local_conds) local_exprs = lappend(local_exprs, ((RestrictInfo *) lfirst(lc))->clause); - fetch_count = GetFetchCountOption(fpstate->table, fpstate->server); - elog(DEBUG1, "relid=%u fetch_count=%d", foreigntableid, fetch_count); - - /* Construct cursor name from sequential value */ - sprintf(name, CURSOR_NAME_FORMAT, cursor_id++); /* ! * Construct CURSOR statements from plain remote query, and make a list ! * contains all of them to pass them to executor with plan node for later ! * use. */ ! sql = fpstate->sql.data; ! fdw_private = lappend(fdw_private, makeString(sql)); ! ! initStringInfo(&cursor); ! appendStringInfo(&cursor, "DECLARE %s SCROLL CURSOR FOR %s", name, sql); ! fdw_private = lappend(fdw_private, makeString(cursor.data)); ! ! initStringInfo(&cursor); ! appendStringInfo(&cursor, "FETCH %d FROM %s", fetch_count, name); ! fdw_private = lappend(fdw_private, makeString(cursor.data)); ! ! initStringInfo(&cursor); ! appendStringInfo(&cursor, "MOVE ABSOLUTE 0 FROM %s", name); ! fdw_private = lappend(fdw_private, makeString(cursor.data)); ! ! initStringInfo(&cursor); ! appendStringInfo(&cursor, "CLOSE %s", name); ! fdw_private = lappend(fdw_private, makeString(cursor.data)); /* * Create the ForeignScan node from target list, local filtering --- 399,410 ---- foreach(lc, fpstate->local_conds) local_exprs = lappend(local_exprs, ((RestrictInfo *) lfirst(lc))->clause); /* ! * Make a list contains SELECT statement to it to executor with plan node ! * for later use. */ ! fdw_private = lappend(fdw_private, makeString(fpstate->sql.data)); /* * Create the ForeignScan node from target list, local filtering *************** pgsqlExplainForeignScan(ForeignScanState *** 477,488 **** List *fdw_private; char *sql; - /* CURSOR declaration is shown in only VERBOSE mode. */ fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; ! if (es->verbose) ! sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql)); ! else ! sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql)); ExplainPropertyText("Remote SQL", sql, es); } --- 433,440 ---- List *fdw_private; char *sql; fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; ! sql = strVal(list_nth(fdw_private, FdwPrivateSelectSql)); ExplainPropertyText("Remote SQL", sql, es); } *************** pgsqlBeginForeignScan(ForeignScanState * *** 514,523 **** festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; /* ! * Create context for per-scan tuplestore under per-query context. */ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt, ! "pgsql_fdw", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); --- 466,480 ---- festate->fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; /* ! * Create contexts for per-scan tuplestore under per-query context. */ festate->scan_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt, ! "pgsql_fdw per-scan data", ! ALLOCSET_DEFAULT_MINSIZE, ! ALLOCSET_DEFAULT_INITSIZE, ! ALLOCSET_DEFAULT_MAXSIZE); ! festate->temp_cxt = AllocSetContextCreate(node->ss.ps.state->es_query_cxt, ! "pgsql_fdw temporary data", ALLOCSET_DEFAULT_MINSIZE, ALLOCSET_DEFAULT_INITSIZE, ALLOCSET_DEFAULT_MAXSIZE); *************** pgsqlIterateForeignScan(ForeignScanState *** 586,705 **** { PgsqlFdwExecutionState *festate; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; - PGresult *res = NULL; MemoryContext oldcontext = CurrentMemoryContext; festate = (PgsqlFdwExecutionState *) node->fdw_state; /* ! * If this is the first call after Begin, we need to execute remote query. ! * ! * Since we needs cursor to prevent out of memory, we declare a cursor at ! * the first call and fetch from it in later calls. */ if (festate->tuples == NULL) execute_query(node); /* ! * If enough tuples are left in tuplestore, just return next tuple from it. * * It is necessary to switch to per-scan context to make returned tuple * valid until next IterateForeignScan call, because it will be released * with ExecClearTuple then. Otherwise, picked tuple is allocated in * per-tuple context, and double-free of that tuple might happen. */ MemoryContextSwitchTo(festate->scan_cxt); ! if (tuplestore_gettupleslot(festate->tuples, true, false, slot)) ! { ! MemoryContextSwitchTo(oldcontext); ! return slot; ! } ! MemoryContextSwitchTo(oldcontext); ! ! /* ! * Here we need to clear partial result and fetch next bunch of tuples from ! * from the cursor for the scan. If the fetch returns no tuple, the scan ! * has reached the end. ! * ! * PGresult must be released before leaving this function. ! */ ! PG_TRY(); ! { ! res = fetch_result(node); ! store_result(node, res); ! PQclear(res); ! res = NULL; ! } ! PG_CATCH(); ! { ! PQclear(res); ! PG_RE_THROW(); ! } ! PG_END_TRY(); ! ! /* ! * If we got more tuples from the server cursor, return next tuple from ! * tuplestore. ! */ ! MemoryContextSwitchTo(festate->scan_cxt); ! if (tuplestore_gettupleslot(festate->tuples, true, false, slot)) ! { ! MemoryContextSwitchTo(oldcontext); ! return slot; ! } MemoryContextSwitchTo(oldcontext); - /* We don't have any result even in remote server cursor. */ - ExecClearTuple(slot); return slot; } /* * pgsqlReScanForeignScan ! * - Restart this scan by resetting fetch location. */ static void pgsqlReScanForeignScan(ForeignScanState *node) { - List *fdw_private; - char *sql; - PGconn *conn; - PGresult *res = NULL; PgsqlFdwExecutionState *festate; festate = (PgsqlFdwExecutionState *) node->fdw_state; ! /* If we have not opened cursor yet, nothing to do. */ if (festate->tuples == NULL) return; ! /* Discard fetch results if any. */ ! tuplestore_clear(festate->tuples); ! ! /* PGresult must be released before leaving this function. */ ! PG_TRY(); ! { ! /* Reset cursor */ ! fdw_private = festate->fdw_private; ! conn = festate->conn; ! sql = strVal(list_nth(fdw_private, FdwPrivateResetSql)); ! res = PQexec(conn, sql); ! if (PQresultStatus(res) != PGRES_COMMAND_OK) ! { ! ereport(ERROR, ! (errmsg("could not rewind cursor"), ! errdetail("%s", PQerrorMessage(conn)), ! errhint("%s", sql))); ! } ! PQclear(res); ! res = NULL; ! } ! PG_CATCH(); ! { ! PQclear(res); ! PG_RE_THROW(); ! } ! PG_END_TRY(); } /* --- 543,596 ---- { PgsqlFdwExecutionState *festate; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; MemoryContext oldcontext = CurrentMemoryContext; festate = (PgsqlFdwExecutionState *) node->fdw_state; /* ! * If this is the first call after Begin or ReScan, we need to execute ! * remote query and get result set. */ if (festate->tuples == NULL) execute_query(node); /* ! * If tuples are still left in tuplestore, just return next tuple from it. * * It is necessary to switch to per-scan context to make returned tuple * valid until next IterateForeignScan call, because it will be released * with ExecClearTuple then. Otherwise, picked tuple is allocated in * per-tuple context, and double-free of that tuple might happen. + * + * If we don't have any result in tuplestore, clear result slot to tell + * executor that this scan is over. */ MemoryContextSwitchTo(festate->scan_cxt); ! tuplestore_gettupleslot(festate->tuples, true, false, slot); MemoryContextSwitchTo(oldcontext); return slot; } /* * pgsqlReScanForeignScan ! * - Restart this scan by clearing old results and set re-execute flag. */ static void pgsqlReScanForeignScan(ForeignScanState *node) { PgsqlFdwExecutionState *festate; festate = (PgsqlFdwExecutionState *) node->fdw_state; ! /* If we haven't have valid result yet, nothing to do. */ if (festate->tuples == NULL) return; ! /* ! * Only rewind the current result set is enough. ! */ ! tuplestore_rescan(festate->tuples); } /* *************** pgsqlReScanForeignScan(ForeignScanState *** 709,718 **** static void pgsqlEndForeignScan(ForeignScanState *node) { - List *fdw_private; - char *sql; - PGconn *conn; - PGresult *res = NULL; PgsqlFdwExecutionState *festate; festate = (PgsqlFdwExecutionState *) node->fdw_state; --- 600,605 ---- *************** pgsqlEndForeignScan(ForeignScanState *no *** 721,763 **** if (festate == NULL) return; ! /* If we have not opened cursor yet, nothing to do. */ ! if (festate->tuples == NULL) ! return; /* Discard fetch results */ ! tuplestore_end(festate->tuples); ! festate->tuples = NULL; ! ! /* PGresult must be released before leaving this function. */ ! PG_TRY(); ! { ! /* Close cursor */ ! fdw_private = festate->fdw_private; ! conn = festate->conn; ! sql = strVal(list_nth(fdw_private, FdwPrivateCloseSql)); ! res = PQexec(conn, sql); ! if (PQresultStatus(res) != PGRES_COMMAND_OK) ! { ! ereport(ERROR, ! (errmsg("could not close cursor"), ! errdetail("%s", PQerrorMessage(conn)), ! errhint("%s", sql))); ! } ! PQclear(res); ! res = NULL; ! } ! PG_CATCH(); { ! PQclear(res); ! PG_RE_THROW(); } - PG_END_TRY(); - - ReleaseConnection(festate->conn); ! MemoryContextDelete(festate->scan_cxt); ! festate->scan_cxt = NULL; } /* --- 608,629 ---- if (festate == NULL) return; ! /* ! * The connection which was used for this scan should be valid until the ! * end of the scan to make the lifespan of remote transaction same as the ! * local query. ! */ ! ReleaseConnection(festate->conn); ! festate->conn = NULL; /* Discard fetch results */ ! if (festate->tuples != NULL) { ! tuplestore_end(festate->tuples); ! festate->tuples = NULL; } ! /* MemoryContext will be deleted automatically. */ } /* *************** adjust_costs(double rows, int width, Cos *** 848,854 **** static void execute_query(ForeignScanState *node) { - List *fdw_private; PgsqlFdwExecutionState *festate; ParamListInfo params = node->ss.ps.state->es_param_list_info; int numParams = params ? params->numParams : 0; --- 714,719 ---- *************** execute_query(ForeignScanState *node) *** 893,925 **** PG_TRY(); { /* ! * Execute remote query with parameters. */ conn = festate->conn; ! fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; ! sql = strVal(list_nth(fdw_private, FdwPrivateDeclareSql)); res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0); /* * If the query has failed, reporting details is enough here. * Connection(s) which are used by this query (at least used by * pgsql_fdw) will be cleaned up by the foreign connection manager. */ ! if (PQresultStatus(res) != PGRES_COMMAND_OK) { ereport(ERROR, ! (errmsg("could not declare cursor"), errdetail("%s", PQerrorMessage(conn)), errhint("%s", sql))); } ! /* Discard result of DECLARE statement. */ ! PQclear(res); ! res = NULL; ! ! /* Fetch first bunch of the result and store them into tuplestore. */ ! res = fetch_result(node); ! store_result(node, res); PQclear(res); res = NULL; } --- 758,795 ---- PG_TRY(); { /* ! * Execute remote query with parameters, and retrieve results with ! * custom row processor which stores results in tuplestore. ! * ! * We uninstall the custom row processor right after processing all ! * results. */ conn = festate->conn; ! sql = strVal(list_nth(festate->fdw_private, FdwPrivateSelectSql)); ! PQsetRowProcessor(conn, query_row_processor, node); res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0); + PQsetRowProcessor(conn, NULL, NULL); + + /* + * We can't know whether the scan is over or not in custom row + * processor, so mark that the result is valid here. + */ + tuplestore_donestoring(festate->tuples); /* * If the query has failed, reporting details is enough here. * Connection(s) which are used by this query (at least used by * pgsql_fdw) will be cleaned up by the foreign connection manager. */ ! if (PQresultStatus(res) != PGRES_TUPLES_OK) { ereport(ERROR, ! (errmsg("could not execute remote query"), errdetail("%s", PQerrorMessage(conn)), errhint("%s", sql))); } ! /* Discard result of SELECT statement. */ PQclear(res); res = NULL; } *************** execute_query(ForeignScanState *node) *** 932,1098 **** } /* - * Fetch next partial result from remote server. - * - * Once this function has returned result records as PGresult, caller is - * responsible to release it, so caller should put codes which might throw - * exception in PG_TRY block. When an exception has been caught, release - * PGresult and re-throw the exception in PG_CATCH block. - */ - static PGresult * - fetch_result(ForeignScanState *node) - { - PgsqlFdwExecutionState *festate; - List *fdw_private; - char *sql; - PGconn *conn; - PGresult *res; - - festate = (PgsqlFdwExecutionState *) node->fdw_state; - - /* Retrieve information for fetching result. */ - fdw_private = festate->fdw_private; - sql = strVal(list_nth(fdw_private, FdwPrivateFetchSql)); - conn = festate->conn; - - /* - * Fetch result from remote server. In error case, we must release - * PGresult in this function to avoid memory leak because caller can't - * get the reference. - */ - res = PQexec(conn, sql); - if (PQresultStatus(res) != PGRES_TUPLES_OK) - { - PQclear(res); - ereport(ERROR, - (errmsg("could not fetch rows from foreign server"), - errdetail("%s", PQerrorMessage(conn)), - errhint("%s", sql))); - } - - return res; - } - - /* * Create tuples from PGresult and store them into tuplestore. * * Caller must use PG_TRY block to catch exception and release PGresult * surely. */ ! static void ! store_result(ForeignScanState *node, PGresult *res) { ! int rows; ! int row; ! int i; ! int nfields; ! int attnum; /* number of non-dropped columns */ ! Form_pg_attribute *attrs; TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; TupleDesc tupdesc = slot->tts_tupleDescriptor; ! PgsqlFdwExecutionState *festate; ! AttInMetadata *attinmeta; ErrorContextCallback errcontext; ! festate = (PgsqlFdwExecutionState *) node->fdw_state; ! rows = PQntuples(res); ! nfields = PQnfields(res); ! attrs = tupdesc->attrs; ! attinmeta = festate->attinmeta; ! ! /* First, ensure that the tuplestore is empty. */ ! if (festate->tuples == NULL) { ! MemoryContext oldcontext = CurrentMemoryContext; ! /* ! * Create tuplestore to store result of the query in per-query context. ! * Note that we use this memory context to avoid memory leak in error ! * cases. ! */ ! MemoryContextSwitchTo(festate->scan_cxt); ! festate->tuples = tuplestore_begin_heap(false, false, work_mem); ! MemoryContextSwitchTo(oldcontext); ! } ! else ! { ! /* We already have tuplestore, just need to clear contents of it. */ ! tuplestore_clear(festate->tuples); ! } ! /* count non-dropped columns */ ! for (attnum = 0, i = 0; i < tupdesc->natts; i++) ! if (!attrs[i]->attisdropped) ! attnum++; ! /* check result and tuple descriptor have the same number of columns */ ! if (attnum > 0 && attnum != nfields) ! ereport(ERROR, ! (errcode(ERRCODE_DATATYPE_MISMATCH), ! errmsg("remote query result rowtype does not match " ! "the specified FROM clause rowtype"), ! errdetail("expected %d, actual %d", attnum, nfields))); /* ! * Set up callback to identify error column. We don't set callback right ! * row because it should be set only during column value conversion. */ ! errcontext.callback = pgsql_fdw_error_callback; ! errcontext.arg = (void *) festate; ! /* put a tuples into the slot */ ! for (row = 0; row < rows; row++) { ! int j; ! HeapTuple tuple; ! /* Install callback function for error. */ ! errcontext.previous = error_context_stack; ! error_context_stack = &errcontext; ! for (i = 0, j = 0; i < tupdesc->natts; i++) { ! /* skip dropped columns. */ ! if (attrs[i]->attisdropped) { ! festate->nulls[i] = true; ! continue; } /* ! * Set NULL indicator, and convert text representation to internal ! * representation if any. */ ! if (PQgetisnull(res, row, j)) ! festate->nulls[i] = true; ! else ! { ! Datum value; ! festate->cur_attno = i + 1; /* first attribute has index 1 */ ! festate->nulls[i] = false; ! value = InputFunctionCall(&attinmeta->attinfuncs[i], ! PQgetvalue(res, row, j), ! attinmeta->attioparams[i], ! attinmeta->atttypmods[i]); ! festate->values[i] = value; ! } ! j++; } ! /* Uninstall error callback function. */ ! error_context_stack = errcontext.previous; ! /* ! * Build the tuple and put it into the slot. ! * We don't have to free the tuple explicitly because it's been ! * allocated in the per-tuple context. ! */ ! tuple = heap_form_tuple(tupdesc, festate->values, festate->nulls); ! tuplestore_puttuple(festate->tuples, tuple); ! } ! tuplestore_donestoring(festate->tuples); } /* --- 802,954 ---- } /* * Create tuples from PGresult and store them into tuplestore. * * Caller must use PG_TRY block to catch exception and release PGresult * surely. */ ! static int ! query_row_processor(PGresult *res, ! const PGdataValue *columns, ! const char **errmsgp, ! void *param) { ! ForeignScanState *node = (ForeignScanState *) param; ! int nfields = PQnfields(res); ! int i; ! int j; ! int attnum; /* number of non-dropped columns */ TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; TupleDesc tupdesc = slot->tts_tupleDescriptor; ! Form_pg_attribute *attrs = tupdesc->attrs; ! PgsqlFdwExecutionState *festate = (PgsqlFdwExecutionState *) node->fdw_state; ! AttInMetadata *attinmeta = festate->attinmeta; ! char *colbuf; ! int colbuflen; ! HeapTuple tuple; ErrorContextCallback errcontext; + MemoryContext oldcontext; ! if (columns == NULL) { ! /* count non-dropped columns */ ! for (attnum = 0, i = 0; i < tupdesc->natts; i++) ! if (!attrs[i]->attisdropped) ! attnum++; ! /* check result and tuple descriptor have the same number of columns */ ! if (attnum > 0 && attnum != nfields) ! ereport(ERROR, ! (errcode(ERRCODE_DATATYPE_MISMATCH), ! errmsg("remote query result rowtype does not match " ! "the specified FROM clause rowtype"), ! errdetail("expected %d, actual %d", attnum, nfields))); ! /* First, ensure that the tuplestore is empty. */ ! if (festate->tuples == NULL) ! { ! /* ! * Create tuplestore to store result of the query in per-query ! * context. Note that we use this memory context to avoid memory ! * leak in error cases. ! */ ! oldcontext = MemoryContextSwitchTo(festate->scan_cxt); ! festate->tuples = tuplestore_begin_heap(false, false, work_mem); ! MemoryContextSwitchTo(oldcontext); ! } ! else ! { ! /* Clear old result just in case. */ ! tuplestore_clear(festate->tuples); ! } ! ! return 1; ! } /* ! * This function is called repeatedly until all result rows are processed, ! * so we should allow interrupt. */ ! CHECK_FOR_INTERRUPTS(); ! /* ! * Do the following work in a temp context that we reset after each tuple. ! * This cleans up not only the data we have direct access to, but any ! * cruft the I/O functions might leak. ! */ ! oldcontext = MemoryContextSwitchTo(festate->temp_cxt); ! ! /* Initialize column value buffer. */ ! colbuflen = 1024; ! colbuf = palloc(colbuflen); ! ! for (i = 0, j = 0; i < tupdesc->natts; i++) { ! int len = columns[j].len; ! /* skip dropped columns. */ ! if (attrs[i]->attisdropped) ! { ! festate->nulls[i] = true; ! continue; ! } ! /* ! * Set NULL indicator, and convert text representation to internal ! * representation if any. ! */ ! if (len < 0) ! festate->nulls[i] = true; ! else { ! Datum value; ! ! festate->nulls[i] = false; ! ! while (colbuflen < len + 1) { ! colbuflen *= 2; ! colbuf = repalloc(colbuf, colbuflen); } + memcpy(colbuf, columns[j].value, len); + colbuf[columns[j].len] = '\0'; /* ! * Set up and install callback to report where convertion error ! * occurs. */ ! festate->cur_attno = i + 1; ! errcontext.callback = pgsql_fdw_error_callback; ! errcontext.arg = (void *) festate; ! errcontext.previous = error_context_stack; ! error_context_stack = &errcontext; ! value = InputFunctionCall(&attinmeta->attinfuncs[i], ! colbuf, ! attinmeta->attioparams[i], ! attinmeta->atttypmods[i]); ! festate->values[i] = value; ! ! /* Uninstall error context callback. */ ! error_context_stack = errcontext.previous; } + j++; + } ! /* ! * Build the tuple and put it into the slot. ! * We don't have to free the tuple explicitly because it's been ! * allocated in the per-tuple context. ! */ ! tuple = heap_form_tuple(tupdesc, festate->values, festate->nulls); ! tuplestore_puttuple(festate->tuples, tuple); ! /* Clean up */ ! MemoryContextSwitchTo(oldcontext); ! MemoryContextReset(festate->temp_cxt); ! return 1; } /* diff --git a/contrib/pgsql_fdw/pgsql_fdw.h b/contrib/pgsql_fdw/pgsql_fdw.h index 5487d52..69b3b2d 100644 *** a/contrib/pgsql_fdw/pgsql_fdw.h --- b/contrib/pgsql_fdw/pgsql_fdw.h *************** int GetFetchCountOption(ForeignTable *ta *** 29,36 **** void deparseSimpleSql(StringInfo buf, Oid relid, PlannerInfo *root, ! RelOptInfo *baserel, ! ForeignTable *table); void appendWhereClause(StringInfo buf, bool has_where, List *exprs, --- 29,35 ---- void deparseSimpleSql(StringInfo buf, Oid relid, PlannerInfo *root, ! RelOptInfo *baserel); void appendWhereClause(StringInfo buf, bool has_where, List *exprs, diff --git a/contrib/pgsql_fdw/sql/pgsql_fdw.sql b/contrib/pgsql_fdw/sql/pgsql_fdw.sql index 6692af7..6e43ea5 100644 *** a/contrib/pgsql_fdw/sql/pgsql_fdw.sql --- b/contrib/pgsql_fdw/sql/pgsql_fdw.sql *************** ALTER SERVER loopback1 OPTIONS ( *** 121,137 **** --replication 'value' ); ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR - ALTER SERVER loopback2 OPTIONS (ADD fetch_count '2'); ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (DROP user, DROP password); ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (host 'value'); -- ERROR ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1'); ! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1', fetch_count '100'); ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR - ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count 'a'); -- ERROR - ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '0'); -- ERROR - ALTER FOREIGN TABLE ft1 OPTIONS (fetch_count '-1'); -- ERROR ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1'); ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1'); --- 121,133 ---- --replication 'value' ); ALTER SERVER loopback1 OPTIONS (user 'value'); -- ERROR ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (DROP user, DROP password); ALTER USER MAPPING FOR public SERVER loopback1 OPTIONS (host 'value'); -- ERROR ALTER FOREIGN TABLE ft1 OPTIONS (nspname 'S 1', relname 'T 1'); ! ALTER FOREIGN TABLE ft2 OPTIONS (nspname 'S 1', relname 'T 1'); ALTER FOREIGN TABLE ft1 OPTIONS (invalid 'value'); -- ERROR ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (invalid 'value'); -- ERROR ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (colname 'C 1'); ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (colname 'C 1'); *************** ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 *** 144,155 **** -- simple queries -- =================================================================== -- single table, with/without alias ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; -- with WHERE clause ! EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; -- aggregate SELECT COUNT(*) FROM ft1 t1; --- 140,151 ---- -- simple queries -- =================================================================== -- single table, with/without alias ! EXPLAIN (COSTS false) SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; SELECT * FROM ft1 ORDER BY c3, c1 OFFSET 100 LIMIT 10; ! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; SELECT * FROM ft1 t1 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; -- with WHERE clause ! EXPLAIN (COSTS false) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1'; -- aggregate SELECT COUNT(*) FROM ft1 t1; diff --git a/doc/src/sgml/pgsql-fdw.sgml b/doc/src/sgml/pgsql-fdw.sgml index eb69f01..ee9c94a 100644 *** a/doc/src/sgml/pgsql-fdw.sgml --- b/doc/src/sgml/pgsql-fdw.sgml *************** *** 97,129 **** - - Cursor Options - - The pgsql_fdw always uses cursor to retrieve the - result from external server. Users can control the behavior of cursor by - setting cursor options to foreign table or foreign server. If an option - is set to both objects, finer-grained setting is used. In other words, - foreign table's setting overrides foreign server's setting. - - - - - - fetch_count - - - This option specifies the number of rows to be fetched at a time. - This option accepts only integer value larger than zero. The default - setting is 10000. - - - - - - - - --- 97,102 ---- *************** postgres=# EXPLAIN SELECT aid FROM pgben *** 249,258 **** Remote SQL: SELECT aid, NULL, abalance, NULL FROM public.pgbench_accounts (3 rows) - - When you specify VERBOSE option, you can see actual DECLARE - statement with cursor name which is used for the scan. - --- 222,227 ----