diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 6c4359d..c9b0d28 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -16803,6 +16803,113 @@ SELECT * FROM pg_ls_dir('.') WITH ORDINALITY AS t(ls,n); + + Cursor Manipulation Functions + + + This section describes functions that cursors to be manipulated + in normal SELECT queries. + + + + The creation of cursors using + or within a procedural language such as + PL/pgSQL + (see ), and other + manipulaton outside of a SELECT query is + described elsewhere in this manual. + + + + <literal><function>ROWS_IN(<parameter>cursor</parameter>)</function></literal> + + + cursor manipulation functions + functions + + + + rows_in + + + +ROWS_IN(cursor REFCURSOR) + RETURNS SETOF RECORD + + + + ROWS_IN retrieves rows from a previously-created + cursor, or a REFCURSOR returned by a function. It may be + considered as equivalent to FETCH ALL, but able + to be integrated into a general query. + + + + ROWS_IN would typically be placed in the + FROM clause. Since the columns specified in the + REFCURSOR may not be determinable at query plan + time, it is required to use a column definition list. For example: + + + +SELECT cur.i, cur.t + FROM + ROWS_IN(fn(...)) AS cur(i int, t text); + + + + The REFCURSOR must be open, and the query must be a + SELECT statement. If the REFCURSOR’s + output does not + correspond to that declared in the query, an error is raised at + query execution time. UPDATE and other DML + statements are not permitted, even if they return a set of rows. + Even if the REFCURSOR returns rows from a physical table, they may + not be SELECTed FOR UPDATE. + + + + It is also possible to place ROWS_IN in the + SELECT list. Tools to manipulate the results from an untyped + RECORD are relatively few, making its use limited in that context. + One exception is row_to_json (see + ). + + + + + It is acceptable to use multiple ROWS_IN constructs + referencing multiple REFCURSORs in the same + query. If several references + are made to the same REFCURSOR, place the construction + inside a WITH subquery + marked MATERIALIZE ALWAYS. + + + + + + When placed in the FROM clause, + ROWS_IN results are staged + before being processed by the executor. Do not rely upon this + behaviour as it may be changed in future. When placed in the + SELECT list, rows are always released one at a time. + + + + + + When the REFCURSOR is returned from a VOLATILE + function, the query associated with the REFCURSOR is + always executed as is. When the REFCURSOR is returned + from a STABLE or IMMUTABLE function, in + future, the planner may be able to inline the REFCURSOR + query into the outer query. + + + + + System Information Functions and Operators diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 2987a55..db1a6bd 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -3427,6 +3427,16 @@ COMMIT; + + Querying cursors + + + Section discusses the use of + ROWS_IN which allows a returned + REFCURSOR to be queried in a normal + SELECT command. + + diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 790d7a2..ae5d6d3 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -81,6 +81,7 @@ OBJS = \ rangetypes_selfuncs.o \ rangetypes_spgist.o \ rangetypes_typanalyze.o \ + refcursor.o \ regexp.o \ regproc.o \ ri_triggers.o \ diff --git a/src/backend/utils/adt/refcursor.c b/src/backend/utils/adt/refcursor.c new file mode 100644 index 0000000..d955c2f --- /dev/null +++ b/src/backend/utils/adt/refcursor.c @@ -0,0 +1,394 @@ +/*------------------------------------------------------------------------- + * + * refcursor.c + * + * IDENTIFICATION + * src/backend/utils/adt/refcursor.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "catalog/pg_type.h" +#include "funcapi.h" +#include "nodes/nodeFuncs.h" +#include "nodes/supportnodes.h" +#include "optimizer/optimizer.h" +#include "tcop/pquery.h" +#include "utils/builtins.h" +#include "utils/datum.h" +#include "utils/lsyscache.h" +#include "utils/refcursor.h" +#include "utils/typcache.h" + + +typedef struct SingleSlotDestReceiver +{ + DestReceiver pub; + TupleTableSlot *received_slot; + TupleDesc tupdesc; +} SingleSlotDestReceiver; + +/* + * sqlfunction_startup --- executor startup + */ +static void +ssdr_startup(DestReceiver *self, int operation, TupleDesc typeinfo) +{ + SingleSlotDestReceiver *myState = (SingleSlotDestReceiver *) self; + + myState->tupdesc = typeinfo; +} + +/* + * sqlfunction_receive --- receive one tuple + */ +static bool +ssdr_receive(TupleTableSlot *slot, DestReceiver *self) +{ + SingleSlotDestReceiver *myState = (SingleSlotDestReceiver *) self; + + myState->received_slot = slot; + + return true; +} + +/* + * sqlfunction_shutdown --- executor end + */ +static void +ssdr_shutdown(DestReceiver *self) +{ + /* no-op */ +} + +/* + * sqlfunction_destroy --- release DestReceiver object + */ +static void +ssdr_destroy(DestReceiver *self) +{ + pfree(self); +} + +/* + * CreateSingleSlotDestReceiver -- create a DestReceiver + * that acquires a single tupleslot + */ +static DestReceiver * +CreateSingleSlotDestReceiver(void) +{ + SingleSlotDestReceiver *self = (SingleSlotDestReceiver *) palloc(sizeof(SingleSlotDestReceiver)); + + self->pub.receiveSlot = ssdr_receive; + self->pub.rStartup = ssdr_startup; + self->pub.rShutdown = ssdr_shutdown; + self->pub.rDestroy = ssdr_destroy; + self->pub.mydest = -1; + + /* private fields will be set by ssdr_startup */ + + return (DestReceiver *) self; +} + +/* + * ROWS_IN (REFCURSOR) + */ +Datum +rows_in_refcursor(PG_FUNCTION_ARGS) +{ + typedef struct + { + Portal portal; + SingleSlotDestReceiver *dest; + TupleDesc tupdesc; + } rows_in_refcursor_fctx; + + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + FuncCallContext *funcctx; + rows_in_refcursor_fctx *fctx; + char *portal_name; + FetchDirection direction; + uint64 howMany; + uint64 nfetched; + bool first_call; + MemoryContext oldcontext; + HeapTuple tuple; + Datum datum; + HeapTupleHeader result; + + /* stuff done only on the first call of the function */ + first_call = SRF_IS_FIRSTCALL(); + if (first_call) + { + /* create a function context for cross-call persistence */ + funcctx = SRF_FIRSTCALL_INIT(); + + /* Check to see if caller supports us returning a set */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_ValuePerCall)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("value per call mode required, but it is not " \ + "allowed in this context"))); + + /* + * switch to memory context appropriate for multiple function calls + */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* allocate memory for user context */ + fctx = (rows_in_refcursor_fctx *) palloc(sizeof(rows_in_refcursor_fctx)); + + fctx->dest = (SingleSlotDestReceiver *) CreateSingleSlotDestReceiver(); + + MemoryContextSwitchTo(oldcontext); + + portal_name = text_to_cstring(PG_GETARG_TEXT_PP(0)); + + fctx->portal = GetPortalByName(portal_name); + + /* Check that the portal exists */ + if (!PortalIsValid(fctx->portal)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_CURSOR), + errmsg("cursor \"%s\" does not exist", portal_name))); + + /* ensure the Portal is ready (has already been OPEN'ed) */ + if (! (fctx->portal->status == PORTAL_DEFINED || + fctx->portal->status == PORTAL_READY)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is not OPEN", portal_name))); + + /* + * Ensure the Portal returns some results (so is not a utility + * command, or set of multiple statements. + */ + if (! (fctx->portal->strategy == PORTAL_ONE_SELECT || + fctx->portal->strategy == PORTAL_ONE_RETURNING || + fctx->portal->strategy == PORTAL_ONE_MOD_WITH)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_DEFINITION), + errmsg("cursor \"%s\" does not return result set", portal_name))); + + pfree(portal_name); + + funcctx->user_fctx = fctx; + } + + /* stuff done on every call of the function */ + funcctx = SRF_PERCALL_SETUP(); + fctx = funcctx->user_fctx; + + rsinfo->returnMode = SFRM_ValuePerCall; + + if (first_call) + { + direction = FETCH_ABSOLUTE; /* start from absolute position */ + howMany = 1; /* position = 1 (top); also reads one row */ + } + else + { + direction = FETCH_FORWARD; /* othrewise advance forward */ + howMany = 1; /* count = 1 (read one row) */ + } + + /* Run the cursor... */ + nfetched = PortalRunFetch (fctx->portal, direction, howMany, + (DestReceiver *) fctx->dest); + + /* + * Initialise the Tuple Desriptor. (This can't be done until + * we have done our first fetch.) + */ + if (first_call) + { + MemoryContext per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; + MemoryContext oldcontext = MemoryContextSwitchTo(per_query_ctx); + + fctx->tupdesc = CreateTupleDescCopy (fctx->dest->tupdesc); + + /* For RECORD results, make sure a typmod has been assigned */ + if (fctx->tupdesc->tdtypeid == RECORDOID && + fctx->tupdesc->tdtypmod < 0) + assign_record_type_typmod(fctx->tupdesc); + + MemoryContextSwitchTo(oldcontext); + } + + rsinfo->setDesc = fctx->tupdesc; + + Assert (nfetched <= 1); + + if (nfetched == 1) + { + /* + * Convert the TableTupleSlot to a HeapTuple (doesn't + * materialise and doesn't copy unless unavoidable). + */ + tuple = ExecFetchSlotHeapTuple (fctx->dest->received_slot, + /* materialise */ false, + NULL); + + /* + * Avoid making a copy if the HeapTuple is already + * fully in memory and marked with correct typeid/typmod. + */ + datum = PointerGetDatum (tuple->t_data); + if (HeapTupleHasExternal(tuple) || + HeapTupleHeaderGetTypeId(tuple->t_data) != fctx->tupdesc->tdtypeid || + HeapTupleHeaderGetTypMod(tuple->t_data) != fctx->tupdesc->tdtypmod) + { + /* + * Copy the tuple as a Datum, ensuring it is + * fully in memory in the process. + */ + datum = heap_copy_tuple_as_datum (tuple, fctx->tupdesc); + } + + /* + * Obtain HeapTupleHeader for the Datum, which is in + * memory, so should not require a copy. + */ + result = DatumGetHeapTupleHeader (datum); + + SRF_RETURN_NEXT (funcctx, PointerGetDatum (result)); + } + else /* no rows retrieved */ + { + /* it will have been pfree()'ed by ssdr_destroy() */ + fctx->dest = NULL; + + /* fctx itself will be released when multi_call_memory_ctx goes. */ + + SRF_RETURN_DONE(funcctx); + } +} + + +/* + * Planner support function for ROWS_IN (REFCURSOR) + */ +Datum +rows_in_refcursor_support(PG_FUNCTION_ARGS) +{ + Node *rawreq = (Node *) PG_GETARG_POINTER(0); + Node *ret; + Node *req_node; + SupportRequestRows *rows_req = NULL; /* keep compiler happy */ + SupportRequestCost *cost_req = NULL; /* keep compiler happy */ + List *args; + Node *arg1; + Const *cexpr; + char *portal_name; + Portal portal; + QueryDesc *qdesc; + PlanState *planstate; + Oid typoutput; + bool typIsVarlena; + + if (IsA(rawreq, SupportRequestRows)) + { + rows_req = (SupportRequestRows *) rawreq; + + req_node = rows_req->node; + } + else if (IsA (rawreq, SupportRequestCost)) + { + cost_req = (SupportRequestCost *) rawreq; + + req_node = cost_req->node; + } + else + PG_RETURN_POINTER(NULL); + + /* The call to ROWS_IN should be in a FuncExpr node. */ + if (!is_funcclause(req_node)) + PG_RETURN_POINTER(NULL); + + args = ((FuncExpr *) req_node)->args; + if (args == NULL) + PG_RETURN_POINTER(NULL); + + arg1 = linitial(args); + + /* + * We can only estimate the cost if the REFCURSOR is + * already simplified to a Const. + */ + if (!IsA (arg1, Const)) + PG_RETURN_POINTER(NULL); + + cexpr = (Const *) arg1; + + if (cexpr->constisnull) + PG_RETURN_POINTER(NULL); + + if (cexpr->consttype != REFCURSOROID) + PG_RETURN_POINTER(NULL); + + /* + * We can ignore a check on the collation because we are not + * interested in sorting, and typemod because REFCURSOR has + * no modifyable attributes. + */ + getTypeOutputInfo(cexpr->consttype, &typoutput, &typIsVarlena); + + portal_name = OidOutputFunctionCall(typoutput, cexpr->constvalue); + + portal = GetPortalByName(portal_name); + + /* Check that the portal exists */ + if (!PortalIsValid(portal)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_CURSOR), + errmsg("cursor \"%s\" does not exist", portal_name))); + + /* ensure the Portal is ready (has already been OPEN'ed) */ + if (! (portal->status == PORTAL_DEFINED || + portal->status == PORTAL_READY)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_STATE), + errmsg("cursor \"%s\" is not OPEN", portal_name))); + + /* + * Ensure the Portal returns some results (so is + * not a utility command, or set of multiple statements. + */ + if (! (portal->strategy == PORTAL_ONE_SELECT || + portal->strategy == PORTAL_ONE_RETURNING || + portal->strategy == PORTAL_ONE_MOD_WITH)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_CURSOR_DEFINITION), + errmsg("cursor \"%s\" does not return result set", portal_name))); + + qdesc = portal->queryDesc; + if (qdesc == NULL) + PG_RETURN_POINTER(NULL); + + planstate = qdesc->planstate; + if (planstate == NULL) + PG_RETURN_POINTER(NULL); + + if (rows_req) + { + rows_req->rows = planstate->plan->plan_rows; + } + else if (cost_req) + { + cost_req->startup = planstate->plan->startup_cost; + cost_req->per_tuple = (planstate->plan->total_cost - planstate->plan->startup_cost); + if (planstate->plan->plan_rows != 0.0) + cost_req->per_tuple /= planstate->plan->plan_rows; + } + + ret = (Node *) rawreq; + + PG_RETURN_POINTER(ret); +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fcf2a12..64fabf2 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -1575,6 +1575,13 @@ { oid => '3996', descr => 'planner support for array_unnest', proname => 'array_unnest_support', prorettype => 'internal', proargtypes => 'internal', prosrc => 'array_unnest_support' }, +{ oid => '12921', descr => 'expand refcursor to set of rows', + proname => 'rows_in', prorows => '100', prosupport => 'rows_in_refcursor_support', + proretset => 't', prorettype => 'record', proargtypes => 'refcursor', + prosrc => 'rows_in_refcursor' }, +{ oid => '12923', descr => 'planner support for rows_in', + proname => 'rows_in_refcursor_support', prorettype => 'internal', + proargtypes => 'internal', prosrc => 'rows_in_refcursor_support' }, { oid => '3167', descr => 'remove any occurrences of an element from an array', proname => 'array_remove', proisstrict => 'f', prorettype => 'anyarray', diff --git a/src/include/utils/refcursor.h b/src/include/utils/refcursor.h new file mode 100644 index 0000000..e636193 --- /dev/null +++ b/src/include/utils/refcursor.h @@ -0,0 +1,15 @@ +/*------------------------------------------------------------------------- +* +* refcursor.c +* +* IDENTIFICATION +* src/include/utils/refcursor.c +* +*------------------------------------------------------------------------- +*/ + +#ifndef refcursor_h +#define refcursor_h + + +#endif /* refcursor_h */ diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index cd2c79f..f8d40eb 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -2078,6 +2078,358 @@ select refcursor_test2(20000, 20000) as "Should be false", f | t (1 row) +-- Check ability to consume from REFCURSOR in SELECT +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int); + QUERY PLAN +------------------------------------------------------ + Function Scan on pg_catalog.rows_in + Output: a + Function Call: rows_in(return_unnamed_refcursor()) +(3 rows) + +select * from rows_in(return_unnamed_refcursor()) as (a int); + a +----- + 5 + 50 + 500 +(3 rows) + +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int) where a >= 50; + QUERY PLAN +------------------------------------------------------ + Function Scan on pg_catalog.rows_in + Output: a + Function Call: rows_in(return_unnamed_refcursor()) + Filter: (rows_in.a >= 50) +(4 rows) + +select * from rows_in(return_unnamed_refcursor()) as (a int) where a >= 50; + a +----- + 50 + 500 +(2 rows) + +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int) order by a desc; + QUERY PLAN +------------------------------------------------------------ + Sort + Output: a + Sort Key: rows_in.a DESC + -> Function Scan on pg_catalog.rows_in + Output: a + Function Call: rows_in(return_unnamed_refcursor()) +(6 rows) + +select * from rows_in(return_unnamed_refcursor()) as (a int) order by a desc; + a +----- + 500 + 50 + 5 +(3 rows) + +-- Check multiple scan of REFCURSOR +explain (verbose, costs off) select * from (select 1 nr union all select 2) as scan, rows_in(return_unnamed_refcursor()) as (a int) order by nr; + QUERY PLAN +------------------------------------------------------------------ + Sort + Output: (1), rows_in.a + Sort Key: (1) + -> Nested Loop + Output: (1), rows_in.a + -> Function Scan on pg_catalog.rows_in + Output: rows_in.a + Function Call: rows_in(return_unnamed_refcursor()) + -> Materialize + Output: (1) + -> Append + -> Result + Output: 1 + -> Result + Output: 2 +(15 rows) + +select * from (select 1 nr union all select 2) as scan, rows_in(return_unnamed_refcursor()) as (a int) order by nr; + nr | a +----+----- + 1 | 5 + 1 | 50 + 1 | 500 + 2 | 5 + 2 | 50 + 2 | 500 +(6 rows) + +-- Check multiple reference to single REFCURSOR +explain (verbose, costs off) + select * from + rows_in(return_unnamed_refcursor()) as a(n int), + rows_in(return_unnamed_refcursor()) as b(n int) + order by a.n, b.n; + QUERY PLAN +------------------------------------------------------------------ + Sort + Output: a.n, b.n + Sort Key: a.n, b.n + -> Nested Loop + Output: a.n, b.n + -> Function Scan on pg_catalog.rows_in a + Output: a.n + Function Call: rows_in(return_unnamed_refcursor()) + -> Function Scan on pg_catalog.rows_in b + Output: b.n + Function Call: rows_in(return_unnamed_refcursor()) +(11 rows) + +select * from + rows_in(return_unnamed_refcursor()) as a(n int), + rows_in(return_unnamed_refcursor()) as b(n int) + order by a.n, b.n; + n | n +-----+----- + 5 | 5 + 5 | 50 + 5 | 500 + 50 | 5 + 50 | 50 + 50 | 500 + 500 | 5 + 500 | 50 + 500 | 500 +(9 rows) + +explain (verbose, costs off) + select r.cur::text, a.n, b.n from + (select return_unnamed_refcursor() cur) r, + rows_in (r.cur) as a(n int), + rows_in (r.cur) as b(n int) + order by r.cur::text, a.n, b.n; + QUERY PLAN +-------------------------------------------------------------------------- + Sort + Output: (((return_unnamed_refcursor()))::text), a.n, b.n + Sort Key: (((return_unnamed_refcursor()))::text), a.n, b.n + -> Nested Loop + Output: ((return_unnamed_refcursor()))::text, a.n, b.n + -> Nested Loop + Output: (return_unnamed_refcursor()), a.n + -> Result + Output: return_unnamed_refcursor() + -> Function Scan on pg_catalog.rows_in a + Output: a.n + Function Call: rows_in((return_unnamed_refcursor())) + -> Function Scan on pg_catalog.rows_in b + Output: b.n + Function Call: rows_in((return_unnamed_refcursor())) +(15 rows) + +select r.cur::text, a.n, b.n from + (select return_unnamed_refcursor() cur) r, + rows_in (r.cur) as a(n int), + rows_in (r.cur) as b(n int) + order by r.cur::text, a.n, b.n; + cur | n | n +---------------------+-----+----- + | 5 | 5 + | 5 | 50 + | 5 | 500 + | 50 | 5 + | 50 | 50 + | 50 | 500 + | 500 | 5 + | 500 | 50 + | 500 | 500 +(9 rows) + +-- Check use of REFCURSOR in WITH +with rcq as ( + select * from rows_in(return_unnamed_refcursor()) as (a int) +) +select * from rcq where a <= 50; + a +---- + 5 + 50 +(2 rows) + +-- Check attempt to UPDATE/DELETE REFCURSOR fails +update rows_in(return_unnamed_refcursor()) set a = 2; +ERROR: syntax error at or near "(" +LINE 1: update rows_in(return_unnamed_refcursor()) set a = 2; + ^ +delete from rows_in(return_unnamed_refcursor()); +ERROR: syntax error at or near "(" +LINE 1: delete from rows_in(return_unnamed_refcursor()); + ^ +-- Check type consistency +select * from rows_in(return_unnamed_refcursor()) as (a int, b int); +ERROR: function return row and query-specified return row do not match +DETAIL: Returned row contains 1 attribute, but query expects 2. +select * from rows_in(return_unnamed_refcursor()) as (a text); +ERROR: function return row and query-specified return row do not match +DETAIL: Returned type integer at ordinal position 1, but query expects text. +select * from rows_in(return_unnamed_refcursor()) as (a jsonb); +ERROR: function return row and query-specified return row do not match +DETAIL: Returned type integer at ordinal position 1, but query expects jsonb. +-- Check consumption from REFCURSOR inside plpgsql FUNCTION +create function refcursor_test3(input refcursor) returns refcursor as $$ +declare + rc refcursor; +begin + open rc for select a, a+1 as a_plus_1 from rows_in(input) as (a int); + return rc; +end +$$ language plpgsql; +explain (verbose, costs off) select * from rows_in(refcursor_test3(return_unnamed_refcursor())) as (a int, ap1 int) where a >= 50; + QUERY PLAN +----------------------------------------------------------------------- + Function Scan on pg_catalog.rows_in + Output: a, ap1 + Function Call: rows_in(refcursor_test3(return_unnamed_refcursor())) + Filter: (rows_in.a >= 50) +(4 rows) + +select * from rows_in(refcursor_test3(return_unnamed_refcursor())) as (a int, ap1 int) where a >= 50; + a | ap1 +-----+----- + 50 | 51 + 500 | 501 +(2 rows) + +drop function refcursor_test3; +-- Check consumption from REFCURSOR defined with USING +create function refcursor_test4(minimum int) returns refcursor as $$ +declare + rc refcursor; +begin + OPEN rc FOR EXECUTE 'select a from rc_test where a >= $1' + USING (minimum); + return rc; +end +$$ language plpgsql; +explain (verbose, costs off) + select * from rows_in(refcursor_test4(50)) as (a int); + QUERY PLAN +----------------------------------------------- + Function Scan on pg_catalog.rows_in + Output: a + Function Call: rows_in(refcursor_test4(50)) +(3 rows) + +select * from rows_in(refcursor_test4(50)) as (a int); + a +----- + 50 + 500 +(2 rows) + +drop function refcursor_test4; +-- Check consumption from REFCURSOR defined in immediate mode +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select * from rows_in('c'::refcursor) as (i int, v text); + i | v +---+----- + 1 | one + 2 | two +(2 rows) + +rollback; +-- Check consumption from REFCURSOR directly in target list (tSRF) +-- (This isn't particularly useful, but it should work.) +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select rows_in('c'::refcursor); + rows_in +--------- + (1,one) + (2,two) +(2 rows) + +rollback; +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select rows_in('c'::refcursor), * from rows_in('c'::refcursor) as (i int, v text); + rows_in | i | v +---------+---+----- + (1,one) | 1 | one + (2,two) | 1 | one + (1,one) | 2 | two + (2,two) | 2 | two +(4 rows) + +rollback; +-- Check use of ROWS_IN with row_to_json both in the target list +-- and in the FROM. +begin; +declare c cursor for + with recursive t as ( + select 1 i + union all + select i + 1 + from t + where i + 1 <= 10 + ) + select i, 'number ' || i t from t; +select row_to_json(rows_in('c'::refcursor)); + row_to_json +-------------------------- + {"i":1,"t":"number 1"} + {"i":2,"t":"number 2"} + {"i":3,"t":"number 3"} + {"i":4,"t":"number 4"} + {"i":5,"t":"number 5"} + {"i":6,"t":"number 6"} + {"i":7,"t":"number 7"} + {"i":8,"t":"number 8"} + {"i":9,"t":"number 9"} + {"i":10,"t":"number 10"} +(10 rows) + +rollback; +begin; +declare c cursor for + with recursive t as ( + select 1 i + union all + select i + 1 + from t + where i + 1 <= 1000 + ) + select i, 'number ' || i t from t; +select j.r, + (j.r->'i')::int i, (j.r->'t')::text t + from (select row_to_json(rows_in('c'::refcursor))::jsonb as r) j + where + (j.r->'i')::int >= 50 + and (j.r->'i')::int <= 60; + r | i | t +-----------------------------+----+------------- + {"i": 50, "t": "number 50"} | 50 | "number 50" + {"i": 51, "t": "number 51"} | 51 | "number 51" + {"i": 52, "t": "number 52"} | 52 | "number 52" + {"i": 53, "t": "number 53"} | 53 | "number 53" + {"i": 54, "t": "number 54"} | 54 | "number 54" + {"i": 55, "t": "number 55"} | 55 | "number 55" + {"i": 56, "t": "number 56"} | 56 | "number 56" + {"i": 57, "t": "number 57"} | 57 | "number 57" + {"i": 58, "t": "number 58"} | 58 | "number 58" + {"i": 59, "t": "number 59"} | 59 | "number 59" + {"i": 60, "t": "number 60"} | 60 | "number 60" +(11 rows) + +rollback; -- -- tests for cursors with named parameter arguments -- diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index d841d8c..2fc76a9 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -1805,6 +1805,143 @@ $$ language plpgsql; select refcursor_test2(20000, 20000) as "Should be false", refcursor_test2(20, 20) as "Should be true"; +-- Check ability to consume from REFCURSOR in SELECT +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int); +select * from rows_in(return_unnamed_refcursor()) as (a int); +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int) where a >= 50; +select * from rows_in(return_unnamed_refcursor()) as (a int) where a >= 50; +explain (verbose, costs off) select * from rows_in(return_unnamed_refcursor()) as (a int) order by a desc; +select * from rows_in(return_unnamed_refcursor()) as (a int) order by a desc; + +-- Check multiple scan of REFCURSOR +explain (verbose, costs off) select * from (select 1 nr union all select 2) as scan, rows_in(return_unnamed_refcursor()) as (a int) order by nr; +select * from (select 1 nr union all select 2) as scan, rows_in(return_unnamed_refcursor()) as (a int) order by nr; + +-- Check multiple reference to single REFCURSOR +explain (verbose, costs off) + select * from + rows_in(return_unnamed_refcursor()) as a(n int), + rows_in(return_unnamed_refcursor()) as b(n int) + order by a.n, b.n; +select * from + rows_in(return_unnamed_refcursor()) as a(n int), + rows_in(return_unnamed_refcursor()) as b(n int) + order by a.n, b.n; +explain (verbose, costs off) + select r.cur::text, a.n, b.n from + (select return_unnamed_refcursor() cur) r, + rows_in (r.cur) as a(n int), + rows_in (r.cur) as b(n int) + order by r.cur::text, a.n, b.n; +select r.cur::text, a.n, b.n from + (select return_unnamed_refcursor() cur) r, + rows_in (r.cur) as a(n int), + rows_in (r.cur) as b(n int) + order by r.cur::text, a.n, b.n; + +-- Check use of REFCURSOR in WITH +with rcq as ( + select * from rows_in(return_unnamed_refcursor()) as (a int) +) +select * from rcq where a <= 50; + +-- Check attempt to UPDATE/DELETE REFCURSOR fails +update rows_in(return_unnamed_refcursor()) set a = 2; +delete from rows_in(return_unnamed_refcursor()); + +-- Check type consistency +select * from rows_in(return_unnamed_refcursor()) as (a int, b int); +select * from rows_in(return_unnamed_refcursor()) as (a text); +select * from rows_in(return_unnamed_refcursor()) as (a jsonb); + +-- Check consumption from REFCURSOR inside plpgsql FUNCTION +create function refcursor_test3(input refcursor) returns refcursor as $$ +declare + rc refcursor; +begin + open rc for select a, a+1 as a_plus_1 from rows_in(input) as (a int); + return rc; +end +$$ language plpgsql; +explain (verbose, costs off) select * from rows_in(refcursor_test3(return_unnamed_refcursor())) as (a int, ap1 int) where a >= 50; +select * from rows_in(refcursor_test3(return_unnamed_refcursor())) as (a int, ap1 int) where a >= 50; + +drop function refcursor_test3; + +-- Check consumption from REFCURSOR defined with USING +create function refcursor_test4(minimum int) returns refcursor as $$ +declare + rc refcursor; +begin + OPEN rc FOR EXECUTE 'select a from rc_test where a >= $1' + USING (minimum); + return rc; +end +$$ language plpgsql; +explain (verbose, costs off) + select * from rows_in(refcursor_test4(50)) as (a int); +select * from rows_in(refcursor_test4(50)) as (a int); + +drop function refcursor_test4; + +-- Check consumption from REFCURSOR defined in immediate mode +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select * from rows_in('c'::refcursor) as (i int, v text); +rollback; + +-- Check consumption from REFCURSOR directly in target list (tSRF) +-- (This isn't particularly useful, but it should work.) +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select rows_in('c'::refcursor); +rollback; +begin; +declare c cursor for + select 1 as i, 'one' v + union all + select 2, 'two'; +select rows_in('c'::refcursor), * from rows_in('c'::refcursor) as (i int, v text); +rollback; + +-- Check use of ROWS_IN with row_to_json both in the target list +-- and in the FROM. +begin; +declare c cursor for + with recursive t as ( + select 1 i + union all + select i + 1 + from t + where i + 1 <= 10 + ) + select i, 'number ' || i t from t; +select row_to_json(rows_in('c'::refcursor)); +rollback; +begin; +declare c cursor for + with recursive t as ( + select 1 i + union all + select i + 1 + from t + where i + 1 <= 1000 + ) + select i, 'number ' || i t from t; +select j.r, + (j.r->'i')::int i, (j.r->'t')::text t + from (select row_to_json(rows_in('c'::refcursor))::jsonb as r) j + where + (j.r->'i')::int >= 50 + and (j.r->'i')::int <= 60; +rollback; + -- -- tests for cursors with named parameter arguments --