From 163d2ba39a0b46deb83e7509d85a5b2012fd84ec Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Tue, 16 Mar 2021 11:28:53 +0100 Subject: [PATCH v2] Dynamic result sets from procedures Declaring a cursor WITH RETURN in a procedure makes the cursor's data be returned as a result of the CALL invocation. The procedure needs to be declared with the DYNAMIC RESULT SETS attribute. Discussion: https://www.postgresql.org/message-id/flat/6e747f98-835f-2e05-cde5-86ee444a7140@2ndquadrant.com --- doc/src/sgml/catalogs.sgml | 10 +++ doc/src/sgml/information_schema.sgml | 3 +- doc/src/sgml/protocol.sgml | 19 +++++ doc/src/sgml/ref/alter_procedure.sgml | 12 +++ doc/src/sgml/ref/create_procedure.sgml | 14 +++ doc/src/sgml/ref/declare.sgml | 34 +++++++- src/backend/catalog/information_schema.sql | 2 +- src/backend/catalog/pg_aggregate.c | 3 +- src/backend/catalog/pg_proc.c | 4 +- src/backend/catalog/sql_features.txt | 2 +- src/backend/commands/functioncmds.c | 75 ++++++++++++++-- src/backend/commands/portalcmds.c | 23 +++++ src/backend/commands/typecmds.c | 12 ++- src/backend/parser/gram.y | 20 ++++- src/backend/tcop/postgres.c | 62 +++++++++++++- src/backend/tcop/pquery.c | 6 ++ src/backend/utils/errcodes.txt | 1 + src/backend/utils/mmgr/portalmem.c | 48 +++++++++++ src/bin/pg_dump/pg_dump.c | 16 +++- src/include/catalog/pg_proc.h | 6 +- src/include/commands/defrem.h | 2 + src/include/nodes/parsenodes.h | 9 +- src/include/parser/kwlist.h | 3 + src/include/utils/portal.h | 14 +++ src/interfaces/libpq/fe-protocol3.c | 6 +- .../regress/expected/create_procedure.out | 85 ++++++++++++++++++- src/test/regress/sql/create_procedure.sql | 61 ++++++++++++- 27 files changed, 518 insertions(+), 34 deletions(-) diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index b1de6d0674..c30d6328ee 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -5844,6 +5844,16 @@ <structname>pg_proc</structname> Columns + + + prodynres int4 + + + For procedures, this records the maximum number of dynamic result sets + the procedure may create. Otherwise zero. + + + pronargs int2 diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml index 4100198252..7f7498eeff 100644 --- a/doc/src/sgml/information_schema.sgml +++ b/doc/src/sgml/information_schema.sgml @@ -5884,7 +5884,8 @@ <structname>routines</structname> Columns max_dynamic_result_sets cardinal_number - Applies to a feature not available in PostgreSQL + For a procedure, the maximum number of dynamic result sets. Otherwise + zero. diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 43092fe62a..4fe0b271e7 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -959,6 +959,25 @@ Extended Query an empty query string), ErrorResponse, or PortalSuspended. + + Executing a portal may give rise to a dynamic result set + sequence. That means the command contained in the portal + created additional result sets beyond what it normally returns. (The + typical example is calling a stored procedure that creates dynamic result + sets.) Dynamic result sets are issues after whatever response the main + command issued. Each dynamic result set begins with a RowDescription + message followed by zero or more DataRow messages. (Since as explained + above an Execute message normally does not respond with a RowDescription, + the appearance of the first RowDescription marks the end of the primary + result set of the portal and the beginning of the first dynamic result + set.) The CommandComplete message that concludes the Execute message + response follows after all dynamic result sets. Note + that dynamic result sets cannot, by their nature, be decribed prior to the + execution of the portal. Multiple executions of the same prepared + statement could result in dynamic result sets with different row + descriptions being returned. + + At completion of each series of extended-query messages, the frontend should issue a Sync message. This parameterless message causes the diff --git a/doc/src/sgml/ref/alter_procedure.sgml b/doc/src/sgml/ref/alter_procedure.sgml index 9cbe2c7cea..92fc83fae2 100644 --- a/doc/src/sgml/ref/alter_procedure.sgml +++ b/doc/src/sgml/ref/alter_procedure.sgml @@ -34,6 +34,7 @@ where action is one of: + DYNAMIC RESULT SETS dynamic_result_sets [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER SET configuration_parameter { TO | = } { value | DEFAULT } SET configuration_parameter FROM CURRENT @@ -150,6 +151,17 @@ Parameters + + DYNAMIC RESULT SETS dynamic_result_sets + + + + Changes the dynamic result sets setting of the procedure. See for more information. + + + + EXTERNAL SECURITY INVOKER EXTERNAL SECURITY DEFINER diff --git a/doc/src/sgml/ref/create_procedure.sgml b/doc/src/sgml/ref/create_procedure.sgml index 6dbc012719..39ff658469 100644 --- a/doc/src/sgml/ref/create_procedure.sgml +++ b/doc/src/sgml/ref/create_procedure.sgml @@ -24,6 +24,7 @@ CREATE [ OR REPLACE ] PROCEDURE name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] ) { LANGUAGE lang_name + | DYNAMIC RESULT SETS dynamic_result_sets | TRANSFORM { FOR TYPE type_name } [, ... ] | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER | SET configuration_parameter { TO value | = value | FROM CURRENT } @@ -173,6 +174,19 @@ Parameters + + DYNAMIC RESULT SETS dynamic_result_sets + + + + Specifies how many dynamic result sets the procedure returns (see + DECLARE WITH + RETURN). The default is 0. If a procedure returns more + result sets than declared, a warning is raised. + + + + TRANSFORM { FOR TYPE type_name } [, ... ] } diff --git a/doc/src/sgml/ref/declare.sgml b/doc/src/sgml/ref/declare.sgml index 2152134635..dded159d18 100644 --- a/doc/src/sgml/ref/declare.sgml +++ b/doc/src/sgml/ref/declare.sgml @@ -27,7 +27,8 @@ DECLARE name [ BINARY ] [ INSENSITIVE ] [ [ NO ] SCROLL ] - CURSOR [ { WITH | WITHOUT } HOLD ] FOR query + CURSOR [ { WITH | WITHOUT } HOLD ] [ { WITH | WITHOUT } RETURN ] + FOR query @@ -120,6 +121,22 @@ Parameters + + WITH RETURN + WITHOUT RETURN + + + This option is only valid for cursors defined inside a procedure. + WITH RETURN specifies that the cursor's result rows + will be provided as a result set of the procedure invocation. To + accomplish that, the cursor must be left open at the end of the + procedure. If multiple WITH RETURN cursors are + declared, then their results will be returned in the order they were + created. WITHOUT RETURN is the default. + + + + query @@ -313,6 +330,21 @@ Examples See for more examples of cursor usage. + + + This example shows how to return multiple result sets from a procedure: + +CREATE PROCEDURE test() +LANGUAGE SQL +AS $$ +DECLARE a CURSOR WITH RETURN FOR SELECT * FROM tbl1; +DECLARE b CURSOR WITH RETURN FOR SELECT * FROM tbl2; +$$; + +CALL test(); + + The results of the two cursors will be returned in order from this call. + diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql index 513cb9a69c..c88a1c4de4 100644 --- a/src/backend/catalog/information_schema.sql +++ b/src/backend/catalog/information_schema.sql @@ -1586,7 +1586,7 @@ CREATE VIEW routines AS CASE WHEN p.proisstrict THEN 'YES' ELSE 'NO' END END AS yes_or_no) AS is_null_call, CAST(null AS character_data) AS sql_path, CAST('YES' AS yes_or_no) AS schema_level_routine, - CAST(0 AS cardinal_number) AS max_dynamic_result_sets, + CAST(p.prodynres AS cardinal_number) AS max_dynamic_result_sets, CAST(null AS yes_or_no) AS is_user_defined_cast, CAST(null AS yes_or_no) AS is_implicitly_invocable, CAST(CASE WHEN p.prosecdef THEN 'DEFINER' ELSE 'INVOKER' END AS character_data) AS security_type, diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c index 89f23d0add..e557d43942 100644 --- a/src/backend/catalog/pg_aggregate.c +++ b/src/backend/catalog/pg_aggregate.c @@ -639,7 +639,8 @@ AggregateCreate(const char *aggName, PointerGetDatum(NULL), /* proconfig */ InvalidOid, /* no prosupport */ 1, /* procost */ - 0); /* prorows */ + 0, /* prorows */ + 0); /* prodynres */ procOid = myself.objectId; /* diff --git a/src/backend/catalog/pg_proc.c b/src/backend/catalog/pg_proc.c index e14eee5a19..05033393a5 100644 --- a/src/backend/catalog/pg_proc.c +++ b/src/backend/catalog/pg_proc.c @@ -91,7 +91,8 @@ ProcedureCreate(const char *procedureName, Datum proconfig, Oid prosupport, float4 procost, - float4 prorows) + float4 prorows, + int dynres) { Oid retval; int parameterCount; @@ -310,6 +311,7 @@ ProcedureCreate(const char *procedureName, values[Anum_pg_proc_proretset - 1] = BoolGetDatum(returnsSet); values[Anum_pg_proc_provolatile - 1] = CharGetDatum(volatility); values[Anum_pg_proc_proparallel - 1] = CharGetDatum(parallel); + values[Anum_pg_proc_prodynres - 1] = Int32GetDatum(dynres); values[Anum_pg_proc_pronargs - 1] = UInt16GetDatum(parameterCount); values[Anum_pg_proc_pronargdefaults - 1] = UInt16GetDatum(list_length(parameterDefaults)); values[Anum_pg_proc_prorettype - 1] = ObjectIdGetDatum(returnType); diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index 32eed988ab..94d0a4494f 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -485,7 +485,7 @@ T433 Multiargument GROUPING function YES T434 GROUP BY DISTINCT NO T441 ABS and MOD functions YES T461 Symmetric BETWEEN predicate YES -T471 Result sets return value NO +T471 Result sets return value NO partially supported T472 DESCRIBE CURSOR NO T491 LATERAL derived table YES T495 Combined data change and retrieval NO different syntax diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c index 7a4e104623..437bddd401 100644 --- a/src/backend/commands/functioncmds.c +++ b/src/backend/commands/functioncmds.c @@ -68,6 +68,7 @@ #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" +#include "utils/portal.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/typcache.h" @@ -478,7 +479,8 @@ compute_common_attribute(ParseState *pstate, DefElem **cost_item, DefElem **rows_item, DefElem **support_item, - DefElem **parallel_item) + DefElem **parallel_item, + DefElem **dynres_item) { if (strcmp(defel->defname, "volatility") == 0) { @@ -554,6 +556,15 @@ compute_common_attribute(ParseState *pstate, *parallel_item = defel; } + else if (strcmp(defel->defname, "dynamic_result_sets") == 0) + { + if (!is_procedure) + goto function_error; + if (*dynres_item) + goto duplicate_error; + + *dynres_item = defel; + } else return false; @@ -567,6 +578,13 @@ compute_common_attribute(ParseState *pstate, parser_errposition(pstate, defel->location))); return false; /* keep compiler quiet */ +function_error: + ereport(ERROR, + (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), + errmsg("invalid attribute in function definition"), + parser_errposition(pstate, defel->location))); + return false; + procedure_error: ereport(ERROR, (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), @@ -703,7 +721,8 @@ compute_function_attributes(ParseState *pstate, float4 *procost, float4 *prorows, Oid *prosupport, - char *parallel_p) + char *parallel_p, + int *dynres_p) { ListCell *option; DefElem *as_item = NULL; @@ -719,6 +738,7 @@ compute_function_attributes(ParseState *pstate, DefElem *rows_item = NULL; DefElem *support_item = NULL; DefElem *parallel_item = NULL; + DefElem *dynres_item = NULL; foreach(option, options) { @@ -776,7 +796,8 @@ compute_function_attributes(ParseState *pstate, &cost_item, &rows_item, &support_item, - ¶llel_item)) + ¶llel_item, + &dynres_item)) { /* recognized common option */ continue; @@ -842,6 +863,11 @@ compute_function_attributes(ParseState *pstate, *prosupport = interpret_func_support(support_item); if (parallel_item) *parallel_p = interpret_func_parallel(parallel_item); + if (dynres_item) + { + *dynres_p = intVal(dynres_item->arg); + Assert(*dynres_p >= 0); /* enforced by parser */ + } } @@ -950,6 +976,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) Form_pg_language languageStruct; List *as_clause; char parallel; + int dynres; /* Convert list of names to a name and namespace */ namespaceId = QualifiedNameGetCreationNamespace(stmt->funcname, @@ -972,6 +999,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) prorows = -1; /* indicates not set */ prosupport = InvalidOid; parallel = PROPARALLEL_UNSAFE; + dynres = 0; /* Extract non-default attributes from stmt->options list */ compute_function_attributes(pstate, @@ -981,7 +1009,7 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) &isWindowFunc, &volatility, &isStrict, &security, &isLeakProof, &proconfig, &procost, &prorows, - &prosupport, ¶llel); + &prosupport, ¶llel, &dynres); /* Look up the language and validate permissions */ languageTuple = SearchSysCache1(LANGNAME, PointerGetDatum(language)); @@ -1170,7 +1198,8 @@ CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) PointerGetDatum(proconfig), prosupport, procost, - prorows); + prorows, + dynres); } /* @@ -1245,6 +1274,7 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) DefElem *rows_item = NULL; DefElem *support_item = NULL; DefElem *parallel_item = NULL; + DefElem *dynres_item = NULL; ObjectAddress address; rel = table_open(ProcedureRelationId, RowExclusiveLock); @@ -1288,7 +1318,8 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) &cost_item, &rows_item, &support_item, - ¶llel_item) == false) + ¶llel_item, + &dynres_item) == false) elog(ERROR, "option \"%s\" not recognized", defel->defname); } @@ -1384,6 +1415,8 @@ AlterFunction(ParseState *pstate, AlterFunctionStmt *stmt) } if (parallel_item) procForm->proparallel = interpret_func_parallel(parallel_item); + if (dynres_item) + procForm->prodynres = intVal(dynres_item->arg); /* Do the update */ CatalogTupleUpdate(rel, &tup->t_self, tup); @@ -2027,6 +2060,24 @@ ExecuteDoStmt(DoStmt *stmt, bool atomic) OidFunctionCall1(laninline, PointerGetDatum(codeblock)); } +static List *procedure_stack; + +Oid +CurrentProcedure(void) +{ + if (!procedure_stack) + return InvalidOid; + else + return llast_oid(procedure_stack); +} + +void +ProcedureCallsCleanup(void) +{ + /* The content of this is freed by memory cleanup. */ + procedure_stack = NIL; +} + /* * Execute CALL statement * @@ -2069,6 +2120,7 @@ ExecuteCallStmt(CallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver char *argmodes; FmgrInfo flinfo; CallContext *callcontext; + int prodynres; EState *estate; ExprContext *econtext; HeapTuple tp; @@ -2109,6 +2161,8 @@ ExecuteCallStmt(CallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver if (((Form_pg_proc) GETSTRUCT(tp))->prosecdef) callcontext->atomic = true; + prodynres = ((Form_pg_proc) GETSTRUCT(tp))->prodynres; + /* * Expand named arguments, defaults, etc. We do not want to scribble on * the passed-in CallStmt parse tree, so first flat-copy fexpr, allowing @@ -2180,9 +2234,11 @@ ExecuteCallStmt(CallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver i++; } + procedure_stack = lappend_oid(procedure_stack, fexpr->funcid); pgstat_init_function_usage(fcinfo, &fcusage); retval = FunctionCallInvoke(fcinfo); pgstat_end_function_usage(&fcusage, true); + procedure_stack = list_delete_last(procedure_stack); if (fexpr->funcresulttype == VOIDOID) { @@ -2230,6 +2286,13 @@ ExecuteCallStmt(CallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver fexpr->funcresulttype); FreeExecutorState(estate); + + CloseOtherReturnableCursors(fexpr->funcid); + + if (list_length(GetReturnableCursors()) > prodynres) + ereport(WARNING, + errcode(ERRCODE_WARNING_ATTEMPT_TO_RETURN_TOO_MANY_RESULT_SETS), + errmsg("attempt to return too many result sets")); } /* diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c index 6f2397bd36..cb7aacd5ce 100644 --- a/src/backend/commands/portalcmds.c +++ b/src/backend/commands/portalcmds.c @@ -24,6 +24,7 @@ #include #include "access/xact.h" +#include "commands/defrem.h" #include "commands/portalcmds.h" #include "executor/executor.h" #include "executor/tstoreReceiver.h" @@ -146,6 +147,28 @@ PerformCursorOpen(ParseState *pstate, DeclareCursorStmt *cstmt, ParamListInfo pa portal->cursorOptions |= CURSOR_OPT_NO_SCROLL; } + /* + * For returnable cursors, remember the currently active procedure, as + * well as the command ID, so we can sort by creation order later. If + * there is no procedure active, the cursor is marked as WITHOUT RETURN. + * (This is not an error, per SQL standard, subclause "Effect of opening a + * cursor".) + */ + if (portal->cursorOptions & CURSOR_OPT_RETURN) + { + Oid procId = CurrentProcedure(); + + if (procId) + { + portal->procId = procId; + portal->createCid = GetCurrentCommandId(true); + } + else + { + portal->cursorOptions &= ~CURSOR_OPT_RETURN; + } + } + /* * Start execution, inserting parameters if any. */ diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c index 76218fb47e..e95105574b 100644 --- a/src/backend/commands/typecmds.c +++ b/src/backend/commands/typecmds.c @@ -1790,7 +1790,8 @@ makeRangeConstructors(const char *name, Oid namespace, PointerGetDatum(NULL), /* proconfig */ InvalidOid, /* prosupport */ 1.0, /* procost */ - 0.0); /* prorows */ + 0.0, /* prorows */ + 0); /* prodynres */ /* * Make the constructors internally-dependent on the range type so @@ -1854,7 +1855,8 @@ makeMultirangeConstructors(const char *name, Oid namespace, PointerGetDatum(NULL), /* proconfig */ InvalidOid, /* prosupport */ 1.0, /* procost */ - 0.0); /* prorows */ + 0.0, /* prorows */ + 0); /* prodynres */ /* * Make the constructor internally-dependent on the multirange type so @@ -1897,7 +1899,8 @@ makeMultirangeConstructors(const char *name, Oid namespace, PointerGetDatum(NULL), /* proconfig */ InvalidOid, /* prosupport */ 1.0, /* procost */ - 0.0); /* prorows */ + 0.0, /* prorows */ + 0); /* prodynres */ /* ditto */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); pfree(argtypes); @@ -1937,7 +1940,8 @@ makeMultirangeConstructors(const char *name, Oid namespace, PointerGetDatum(NULL), /* proconfig */ InvalidOid, /* prosupport */ 1.0, /* procost */ - 0.0); /* prorows */ + 0.0, /* prorows */ + 0); /* prodynres */ /* ditto */ recordDependencyOn(&myself, &referenced, DEPENDENCY_INTERNAL); pfree(argtypes); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 652be0b96d..9e13424ff4 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -640,7 +640,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P - DOUBLE_P DROP + DOUBLE_P DROP DYNAMIC EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION @@ -685,7 +685,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); RANGE READ REAL REASSIGN RECHECK RECURSIVE REF REFERENCES REFERENCING REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA - RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP + RESET RESTART RESTRICT RESULT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES @@ -7834,6 +7834,10 @@ common_func_opt_item: { $$ = makeDefElem("parallel", (Node *)makeString($2), @1); } + | DYNAMIC RESULT SETS Iconst + { + $$ = makeDefElem("dynamic_result_sets", (Node *)makeInteger($4), @1); + } ; createfunc_opt_item: @@ -11125,6 +11129,12 @@ cursor_options: /*EMPTY*/ { $$ = 0; } opt_hold: /* EMPTY */ { $$ = 0; } | WITH HOLD { $$ = CURSOR_OPT_HOLD; } | WITHOUT HOLD { $$ = 0; } + | WITH HOLD WITH RETURN { $$ = CURSOR_OPT_HOLD | CURSOR_OPT_RETURN; } + | WITHOUT HOLD WITH RETURN { $$ = CURSOR_OPT_RETURN; } + | WITH HOLD WITHOUT RETURN { $$ = CURSOR_OPT_HOLD; } + | WITHOUT HOLD WITHOUT RETURN { $$ = 0; } + | WITH RETURN { $$ = CURSOR_OPT_RETURN; } + | WITHOUT RETURN { $$ = 0; } ; /***************************************************************************** @@ -15331,6 +15341,7 @@ unreserved_keyword: | DOMAIN_P | DOUBLE_P | DROP + | DYNAMIC | EACH | ENABLE_P | ENCODING @@ -15472,6 +15483,8 @@ unreserved_keyword: | RESET | RESTART | RESTRICT + | RESULT + | RETURN | RETURNS | REVOKE | ROLE @@ -15867,6 +15880,7 @@ bare_label_keyword: | DOMAIN_P | DOUBLE_P | DROP + | DYNAMIC | EACH | ELSE | ENABLE_P @@ -16050,6 +16064,8 @@ bare_label_keyword: | RESET | RESTART | RESTRICT + | RESULT + | RETURN | RETURNS | REVOKE | RIGHT diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8a0332dde9..56dc35c68e 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -41,6 +41,7 @@ #include "access/xact.h" #include "catalog/pg_type.h" #include "commands/async.h" +#include "commands/defrem.h" #include "commands/prepare.h" #include "executor/spi.h" #include "jit/jit.h" @@ -1009,6 +1010,7 @@ exec_simple_query(const char *query_string) Portal portal; DestReceiver *receiver; int16 format; + ListCell *lc; /* * Get the command name for use in status display (it also becomes the @@ -1168,7 +1170,7 @@ exec_simple_query(const char *query_string) MemoryContextSwitchTo(oldcontext); /* - * Run the portal to completion, and then drop it (and the receiver). + * Run the portal to completion, and then drop it. */ (void) PortalRun(portal, FETCH_ALL, @@ -1178,10 +1180,34 @@ exec_simple_query(const char *query_string) receiver, &qc); - receiver->rDestroy(receiver); - PortalDrop(portal, false); + /* + * Run portals for dynamic result sets. + */ + foreach (lc, GetReturnableCursors()) + { + Portal portal = lfirst(lc); + + if (dest == DestRemote) + SetRemoteDestReceiverParams(receiver, portal); + + PortalRun(portal, + FETCH_ALL, + true, + true, + receiver, + receiver, + NULL); + + PortalDrop(portal, false); + } + + /* + * Drop the receiver. + */ + receiver->rDestroy(receiver); + if (lnext(parsetree_list, parsetree_item) == NULL) { /* @@ -1982,6 +2008,7 @@ exec_execute_message(const char *portal_name, long max_rows) const char *sourceText; const char *prepStmtName; ParamListInfo portalParams; + ListCell *lc; bool save_log_statement_stats = log_statement_stats; bool is_xact_command; bool execute_is_fetch; @@ -2134,6 +2161,34 @@ exec_execute_message(const char *portal_name, long max_rows) receiver, &qc); + /* + * Run portals for dynamic result sets. + */ + foreach (lc, GetReturnableCursors()) + { + Portal dyn_portal = lfirst(lc); + + if (dest == DestRemote) + SetRemoteDestReceiverParams(receiver, dyn_portal); + + PortalSetResultFormat(dyn_portal, 1, &portal->dynamic_format); + + SendRowDescriptionMessage(&row_description_buf, + dyn_portal->tupDesc, + FetchPortalTargetList(dyn_portal), + dyn_portal->formats); + + PortalRun(dyn_portal, + FETCH_ALL, + true, + true, + receiver, + receiver, + NULL); + + PortalDrop(dyn_portal, false); + } + receiver->rDestroy(receiver); /* Done executing; remove the params error callback */ @@ -4083,6 +4138,7 @@ PostgresMain(int argc, char *argv[], PortalErrorCleanup(); SPICleanup(); + ProcedureCallsCleanup(); /* * We can't release replication slots inside AbortTransaction() as we diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 44f5fe8fc9..4d6be24e30 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -629,6 +629,8 @@ PortalSetResultFormat(Portal portal, int nFormats, int16 *formats) errmsg("bind message has %d result formats but query has %d columns", nFormats, natts))); memcpy(portal->formats, formats, natts * sizeof(int16)); + + portal->dynamic_format = 0; } else if (nFormats > 0) { @@ -637,12 +639,16 @@ PortalSetResultFormat(Portal portal, int nFormats, int16 *formats) for (i = 0; i < natts; i++) portal->formats[i] = format1; + + portal->dynamic_format = format1; } else { /* use default format for all columns */ for (i = 0; i < natts; i++) portal->formats[i] = 0; + + portal->dynamic_format = 0; } } diff --git a/src/backend/utils/errcodes.txt b/src/backend/utils/errcodes.txt index 9874a77805..a4504b6436 100644 --- a/src/backend/utils/errcodes.txt +++ b/src/backend/utils/errcodes.txt @@ -83,6 +83,7 @@ Section: Class 01 - Warning # do not use this class for failure conditions 01000 W ERRCODE_WARNING warning 0100C W ERRCODE_WARNING_DYNAMIC_RESULT_SETS_RETURNED dynamic_result_sets_returned +0100E W ERRCODE_WARNING_ATTEMPT_TO_RETURN_TOO_MANY_RESULT_SETS attempt_to_return_too_many_result_sets 01008 W ERRCODE_WARNING_IMPLICIT_ZERO_BIT_PADDING implicit_zero_bit_padding 01003 W ERRCODE_WARNING_NULL_VALUE_ELIMINATED_IN_SET_FUNCTION null_value_eliminated_in_set_function 01007 W ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED privilege_not_granted diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c index 66e3181815..395684bf54 100644 --- a/src/backend/utils/mmgr/portalmem.c +++ b/src/backend/utils/mmgr/portalmem.c @@ -1278,3 +1278,51 @@ HoldPinnedPortals(void) } } } + +static int +cmp_portals_by_creation(const ListCell *a, const ListCell *b) +{ + Portal pa = lfirst(a); + Portal pb = lfirst(b); + + return pa->createCid - pb->createCid; +} + +List * +GetReturnableCursors(void) +{ + List *ret = NIL; + HASH_SEQ_STATUS status; + PortalHashEnt *hentry; + + hash_seq_init(&status, PortalHashTable); + + while ((hentry = (PortalHashEnt *) hash_seq_search(&status)) != NULL) + { + Portal portal = hentry->portal; + + if (portal->cursorOptions & CURSOR_OPT_RETURN) + ret = lappend(ret, portal); + } + + list_sort(ret, cmp_portals_by_creation); + + return ret; +} + +void +CloseOtherReturnableCursors(Oid procid) +{ + HASH_SEQ_STATUS status; + PortalHashEnt *hentry; + + hash_seq_init(&status, PortalHashTable); + + while ((hentry = (PortalHashEnt *) hash_seq_search(&status)) != NULL) + { + Portal portal = hentry->portal; + + if (portal->cursorOptions & CURSOR_OPT_RETURN && portal->procId != procid) + PortalDrop(portal, false); + } +} diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index eb988d7eb4..787edc92a2 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -11996,6 +11996,7 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) char *prorows; char *prosupport; char *proparallel; + int prodynres; char *lanname; char *rettypename; int nallargs; @@ -12090,10 +12091,17 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) if (fout->remoteVersion >= 120000) appendPQExpBufferStr(query, - "prosupport\n"); + "prosupport,\n"); else appendPQExpBufferStr(query, - "'-' AS prosupport\n"); + "'-' AS prosupport,\n"); + + if (fout->remoteVersion >= 140000) + appendPQExpBufferStr(query, + "prodynres\n"); + else + appendPQExpBufferStr(query, + "0 AS prodynres\n"); appendPQExpBuffer(query, "FROM pg_catalog.pg_proc " @@ -12133,6 +12141,7 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) prorows = PQgetvalue(res, 0, PQfnumber(res, "prorows")); prosupport = PQgetvalue(res, 0, PQfnumber(res, "prosupport")); proparallel = PQgetvalue(res, 0, PQfnumber(res, "proparallel")); + prodynres = atoi(PQgetvalue(res, 0, PQfnumber(res, "prodynres"))); lanname = PQgetvalue(res, 0, PQfnumber(res, "lanname")); /* @@ -12309,6 +12318,9 @@ dumpFunc(Archive *fout, const FuncInfo *finfo) if (proisstrict[0] == 't') appendPQExpBufferStr(q, " STRICT"); + if (prodynres > 0) + appendPQExpBuffer(q, " DYNAMIC RESULT SETS %d", prodynres); + if (prosecdef[0] == 't') appendPQExpBufferStr(q, " SECURITY DEFINER"); diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 78f230894b..7fa4d099bd 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -76,6 +76,9 @@ CATALOG(pg_proc,1255,ProcedureRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(81,Proce /* see PROPARALLEL_ categories below */ char proparallel BKI_DEFAULT(s); + /* maximum number of dynamic result sets */ + int32 prodynres BKI_DEFAULT(0); + /* number of arguments */ /* Note: need not be given in pg_proc.dat; genbki.pl will compute it */ int16 pronargs; @@ -209,7 +212,8 @@ extern ObjectAddress ProcedureCreate(const char *procedureName, Datum proconfig, Oid prosupport, float4 procost, - float4 prorows); + float4 prorows, + int dynres); extern bool function_parse_error_transpose(const char *prosrc); diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 1a79540c94..030a7d9011 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -57,6 +57,8 @@ extern ObjectAddress CreateTransform(CreateTransformStmt *stmt); extern void IsThereFunctionInNamespace(const char *proname, int pronargs, oidvector *proargtypes, Oid nspOid); extern void ExecuteDoStmt(DoStmt *stmt, bool atomic); +extern Oid CurrentProcedure(void); +extern void ProcedureCallsCleanup(void); extern void ExecuteCallStmt(CallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver *dest); extern TupleDesc CallStmtResultDesc(CallStmt *stmt); extern Oid get_transform_oid(Oid type_id, Oid lang_id, bool missing_ok); diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 236832a2ca..f335ecba07 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2752,11 +2752,12 @@ typedef struct SecLabelStmt #define CURSOR_OPT_NO_SCROLL 0x0004 /* NO SCROLL explicitly given */ #define CURSOR_OPT_INSENSITIVE 0x0008 /* INSENSITIVE */ #define CURSOR_OPT_HOLD 0x0010 /* WITH HOLD */ +#define CURSOR_OPT_RETURN 0x0020 /* WITH RETURN */ /* these planner-control flags do not correspond to any SQL grammar: */ -#define CURSOR_OPT_FAST_PLAN 0x0020 /* prefer fast-start plan */ -#define CURSOR_OPT_GENERIC_PLAN 0x0040 /* force use of generic plan */ -#define CURSOR_OPT_CUSTOM_PLAN 0x0080 /* force use of custom plan */ -#define CURSOR_OPT_PARALLEL_OK 0x0100 /* parallel mode OK */ +#define CURSOR_OPT_FAST_PLAN 0x0100 /* prefer fast-start plan */ +#define CURSOR_OPT_GENERIC_PLAN 0x0200 /* force use of generic plan */ +#define CURSOR_OPT_CUSTOM_PLAN 0x0400 /* force use of custom plan */ +#define CURSOR_OPT_PARALLEL_OK 0x0800 /* parallel mode OK */ typedef struct DeclareCursorStmt { diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 28083aaac9..9e1f81ac7d 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -141,6 +141,7 @@ PG_KEYWORD("document", DOCUMENT_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("domain", DOMAIN_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("dynamic", DYNAMIC, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL) @@ -346,6 +347,8 @@ PG_KEYWORD("replica", REPLICA, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("reset", RESET, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("restart", RESTART, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("restrict", RESTRICT, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("result", RESULT, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("return", RETURN, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("returning", RETURNING, RESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("returns", RETURNS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("revoke", REVOKE, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/utils/portal.h b/src/include/utils/portal.h index 3c17b039cc..97a9e9bf53 100644 --- a/src/include/utils/portal.h +++ b/src/include/utils/portal.h @@ -131,6 +131,16 @@ typedef struct PortalData SubTransactionId createSubid; /* the creating subxact */ SubTransactionId activeSubid; /* the last subxact with activity */ + /* + * Procedure that created this portal. Used for returnable cursors. + */ + Oid procId; + /* + * Command ID where the portal was created. Used for sorting returnable + * cursors into creation order. + */ + CommandId createCid; + /* The query or queries the portal will execute */ const char *sourceText; /* text of query (as of 8.4, never NULL) */ CommandTag commandTag; /* command tag for original query */ @@ -159,6 +169,8 @@ typedef struct PortalData TupleDesc tupDesc; /* descriptor for result tuples */ /* and these are the format codes to use for the columns: */ int16 *formats; /* a format code for each column */ + /* Format code for dynamic result sets */ + int16 dynamic_format; /* * Where we store tuples for a held cursor or a PORTAL_ONE_RETURNING or @@ -237,5 +249,7 @@ extern void PortalCreateHoldStore(Portal portal); extern void PortalHashTableDeleteAll(void); extern bool ThereAreNoReadyPortals(void); extern void HoldPinnedPortals(void); +extern List *GetReturnableCursors(void); +extern void CloseOtherReturnableCursors(Oid procid); #endif /* PORTAL_H */ diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 306e89acfd..ebd38c391e 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -337,10 +337,8 @@ pqParseInput3(PGconn *conn) { /* * A new 'T' message is treated as the start of - * another PGresult. (It is not clear that this is - * really possible with the current backend.) We stop - * parsing until the application accepts the current - * result. + * another PGresult. We stop parsing until the + * application accepts the current result. */ conn->asyncStatus = PGASYNC_READY; return; diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out index 3838fa2324..39f134bc82 100644 --- a/src/test/regress/expected/create_procedure.out +++ b/src/test/regress/expected/create_procedure.out @@ -212,8 +212,91 @@ ALTER ROUTINE cp_testfunc1a RENAME TO cp_testfunc1; ALTER ROUTINE ptest1(text) RENAME TO ptest1a; ALTER ROUTINE ptest1a RENAME TO ptest1; DROP ROUTINE cp_testfunc1(int); +-- dynamic result sets +CREATE TABLE cp_test2 (a int); +INSERT INTO cp_test2 VALUES (1), (2), (3); +CREATE TABLE cp_test3 (x text, y text); +INSERT INTO cp_test3 VALUES ('abc', 'def'), ('foo', 'bar'); +CREATE PROCEDURE pdrstest1() +LANGUAGE SQL +DYNAMIC RESULT SETS 2 +AS $$ +DECLARE c1 CURSOR WITH RETURN FOR SELECT * FROM cp_test2; +DECLARE c2 CURSOR WITH RETURN FOR SELECT * FROM cp_test3; +$$; +CALL pdrstest1(); + a +--- + 1 + 2 + 3 +(3 rows) + + x | y +-----+----- + abc | def + foo | bar +(2 rows) + +CREATE PROCEDURE pdrstest2() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +CALL pdrstest1(); +DECLARE c3 CURSOR WITH RETURN FOR SELECT * FROM cp_test2 WHERE a < 2; +$$; +CALL pdrstest2(); + a +--- + 1 +(1 row) + +CREATE PROCEDURE pdrstest3(INOUT a text) +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +DECLARE c4 CURSOR WITH RETURN FOR SELECT * FROM cp_test2; +SELECT a || a; +$$; +CALL pdrstest3('x'); + a +---- + xx +(1 row) + + a +--- + 1 + 2 + 3 +(3 rows) + +-- test the nested error handling +CREATE TABLE cp_test_dummy (a int); +CREATE PROCEDURE pdrstest4a() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +DECLARE c5a CURSOR WITH RETURN FOR SELECT * FROM cp_test_dummy; +$$; +CREATE PROCEDURE pdrstest4b() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +CALL pdrstest4a(); +$$; +DROP TABLE cp_test_dummy; +CALL pdrstest4b(); +ERROR: relation "cp_test_dummy" does not exist +LINE 2: DECLARE c5a CURSOR WITH RETURN FOR SELECT * FROM cp_test_dum... + ^ +QUERY: +DECLARE c5a CURSOR WITH RETURN FOR SELECT * FROM cp_test_dummy; + +CONTEXT: SQL function "pdrstest4a" during startup +SQL function "pdrstest4b" statement 1 -- cleanup DROP PROCEDURE ptest1; DROP PROCEDURE ptest2; -DROP TABLE cp_test; +DROP TABLE cp_test, cp_test2, cp_test3; DROP USER regress_cp_user1; diff --git a/src/test/regress/sql/create_procedure.sql b/src/test/regress/sql/create_procedure.sql index 2ef1c82cea..761334df5b 100644 --- a/src/test/regress/sql/create_procedure.sql +++ b/src/test/regress/sql/create_procedure.sql @@ -167,11 +167,70 @@ CREATE USER regress_cp_user1; DROP ROUTINE cp_testfunc1(int); +-- dynamic result sets + +CREATE TABLE cp_test2 (a int); +INSERT INTO cp_test2 VALUES (1), (2), (3); +CREATE TABLE cp_test3 (x text, y text); +INSERT INTO cp_test3 VALUES ('abc', 'def'), ('foo', 'bar'); + +CREATE PROCEDURE pdrstest1() +LANGUAGE SQL +DYNAMIC RESULT SETS 2 +AS $$ +DECLARE c1 CURSOR WITH RETURN FOR SELECT * FROM cp_test2; +DECLARE c2 CURSOR WITH RETURN FOR SELECT * FROM cp_test3; +$$; + +CALL pdrstest1(); + +CREATE PROCEDURE pdrstest2() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +CALL pdrstest1(); +DECLARE c3 CURSOR WITH RETURN FOR SELECT * FROM cp_test2 WHERE a < 2; +$$; + +CALL pdrstest2(); + +CREATE PROCEDURE pdrstest3(INOUT a text) +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +DECLARE c4 CURSOR WITH RETURN FOR SELECT * FROM cp_test2; +SELECT a || a; +$$; + +CALL pdrstest3('x'); + +-- test the nested error handling +CREATE TABLE cp_test_dummy (a int); + +CREATE PROCEDURE pdrstest4a() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +DECLARE c5a CURSOR WITH RETURN FOR SELECT * FROM cp_test_dummy; +$$; + +CREATE PROCEDURE pdrstest4b() +LANGUAGE SQL +DYNAMIC RESULT SETS 1 +AS $$ +CALL pdrstest4a(); +$$; + +DROP TABLE cp_test_dummy; + +CALL pdrstest4b(); + + -- cleanup DROP PROCEDURE ptest1; DROP PROCEDURE ptest2; -DROP TABLE cp_test; +DROP TABLE cp_test, cp_test2, cp_test3; DROP USER regress_cp_user1; -- 2.30.2