From: Tom Lane Date: Sun, 1 Jan 2023 18:22:34 +0000 (-0500) Subject: In plpgsql, don't preassign portal names to bound cursor variables. X-Git-Url: http://git.postgresql.org/gitweb/-?a=commitdiff_plain;h=d747dc85aec536c471fd7c739695e155627b08fd;p=users%2Frhaas%2Fpostgres.git In plpgsql, don't preassign portal names to bound cursor variables. A refcursor variable that is bound to a specific query (by declaring it with "CURSOR FOR") now chooses a portal name in the same way as an unbound, plain refcursor variable. Its string value starts out as NULL, and unless that's overridden by manual assignment, it will be replaced by a unique-within-session portal name during OPEN. The previous behavior was to initialize such variables to contain their own name, resulting in that also being the portal name unless the user overwrote it before OPEN. The trouble with this is that it causes failures due to conflicting portal names if the same cursor variable name is used in different functions. It is pretty non-orthogonal to have bound and unbound refcursor variables behave differently on this point, too, so let's change it. This change can cause compatibility problems for applications that open a bound cursor in a plpgsql function and then use it in the calling code without explicitly passing back the refcursor value (portal name). If the calling code simply assumes that the portal name matches the called function's variable name, it will now fail. That can be fixed by explicitly assigning a string value to the refcursor variable before OPEN, e.g. DECLARE myc CURSOR FOR SELECT ...; BEGIN myc := 'myc'; -- add this OPEN myc; We have no documentation examples showing the troublesome usage pattern, so we can hope it's rare in practice. Patch by me; thanks to Pavel Stehule and Jan Wieck for review. Discussion: https://postgr.es/m/1465101.1667345983@sss.pgh.pa.us --- diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml index 2b2a1a8215..7fc8d1467f 100644 --- a/doc/src/sgml/plpgsql.sgml +++ b/doc/src/sgml/plpgsql.sgml @@ -3177,7 +3177,9 @@ DECLARE Before a cursor can be used to retrieve rows, it must be opened. (This is the equivalent action to the SQL - command DECLARE CURSOR.) PL/pgSQL has + command DECLARE + CURSOR.) + PL/pgSQL has three forms of the OPEN statement, two of which use unbound cursor variables while the third uses a bound cursor variable. @@ -3187,9 +3189,28 @@ DECLARE Bound cursor variables can also be used without explicitly opening the cursor, via the FOR statement described in . + A FOR loop will open the cursor and then + close it again when the loop completes. + + portal + in PL/pgSQL + + + + Opening a cursor involves creating a server-internal data structure + called a portal, which holds the execution + state for the cursor's query. A portal has a name, which must be + unique within the session for the duration of the portal's existence. + By default, PL/pgSQL will assign a unique + name to each portal it creates. However, if you assign a non-null + string value to a cursor variable, that string will be used as its + portal name. This feature can be used as described in + . + + <command>OPEN FOR</command> <replaceable>query</replaceable> @@ -3338,7 +3359,7 @@ BEGIN opened the cursor to begin with. You can return a refcursor value out of a function and let the caller operate on the cursor. (Internally, a refcursor value is simply the string name - of a so-called portal containing the active query for the cursor. This name + of the portal containing the active query for the cursor. This name can be passed around, assigned to other refcursor variables, and so on, without disturbing the portal.) @@ -3480,7 +3501,7 @@ CLOSE curs1; - + Returning Cursors @@ -3500,7 +3521,8 @@ CLOSE curs1; simply assign a string to the refcursor variable before opening it. The string value of the refcursor variable will be used by OPEN as the name of the underlying portal. - However, if the refcursor variable is null, + However, if the refcursor variable's value is null + (as it will be by default), then OPEN automatically generates a name that does not conflict with any existing portal, and assigns it to the refcursor variable. @@ -3508,12 +3530,12 @@ CLOSE curs1; - A bound cursor variable is initialized to the string value - representing its name, so that the portal name is the same as - the cursor variable name, unless the programmer overrides it - by assignment before opening the cursor. But an unbound cursor - variable defaults to the null value initially, so it will receive - an automatically-generated unique name, unless overridden. + Prior to PostgreSQL 16, bound cursor + variables were initialized to contain their own names, rather + than being left as null, so that the underlying portal name would + be the same as the cursor variable's name by default. This was + changed because it created too much risk of conflicts between + similarly-named cursors in different functions. diff --git a/doc/src/sgml/ref/declare.sgml b/doc/src/sgml/ref/declare.sgml index bbbd335bd0..5712825314 100644 --- a/doc/src/sgml/ref/declare.sgml +++ b/doc/src/sgml/ref/declare.sgml @@ -13,6 +13,11 @@ PostgreSQL documentation DECLARE + + portal + DECLARE + + DECLARE 7 @@ -61,6 +66,8 @@ DECLARE name [ BINARY ] [ ASENSITIV The name of the cursor to be created. + This must be different from any other active cursor name in the + session. @@ -305,6 +312,15 @@ DECLARE name [ BINARY ] [ ASENSITIV DECLARE and OPEN statements. + + The server data structure underlying an open cursor is called a + portal. Portal names are exposed in the + client protocol: a client can fetch rows directly from an open + portal, if it knows the portal name. When creating a cursor with + DECLARE, the portal name is the same as the + cursor name. + + You can see all available cursors by querying the pg_cursors diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y index fe63766e5d..a9de7936ce 100644 --- a/src/pl/plpgsql/src/pl_gram.y +++ b/src/pl/plpgsql/src/pl_gram.y @@ -534,10 +534,6 @@ decl_statement : decl_varname decl_const decl_datatype decl_collate decl_notnull decl_cursor_args decl_is_for decl_cursor_query { PLpgSQL_var *new; - PLpgSQL_expr *curname_def; - char buf[NAMEDATALEN * 2 + 64]; - char *cp1; - char *cp2; /* pop local namespace for cursor args */ plpgsql_ns_pop(); @@ -550,29 +546,6 @@ decl_statement : decl_varname decl_const decl_datatype decl_collate decl_notnull NULL), true); - curname_def = palloc0(sizeof(PLpgSQL_expr)); - - /* Note: refname has been truncated to NAMEDATALEN */ - cp1 = new->refname; - cp2 = buf; - /* - * Don't trust standard_conforming_strings here; - * it might change before we use the string. - */ - if (strchr(cp1, '\\') != NULL) - *cp2++ = ESCAPE_STRING_SYNTAX; - *cp2++ = '\''; - while (*cp1) - { - if (SQL_STR_DOUBLE(*cp1, true)) - *cp2++ = *cp1; - *cp2++ = *cp1++; - } - strcpy(cp2, "'::pg_catalog.refcursor"); - curname_def->query = pstrdup(buf); - curname_def->parseMode = RAW_PARSE_PLPGSQL_EXPR; - new->default_val = curname_def; - new->cursor_explicit_expr = $7; if ($5 == NULL) new->cursor_explicit_argrow = -1; diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out index 08e42f17dc..cdc519256a 100644 --- a/src/test/regress/expected/plpgsql.out +++ b/src/test/regress/expected/plpgsql.out @@ -3482,6 +3482,9 @@ declare c2 cursor for select * from generate_series(41,43) i; begin + -- assign portal names to cursors to get stable output + c := 'c'; + c2 := 'c2'; for r in c(5,7) loop raise notice '% from %', r.i, c; end loop; @@ -3624,6 +3627,22 @@ select * from forc_test; (10 rows) drop function forc01(); +-- it's okay to re-use a cursor variable name, even when bound +do $$ +declare cnt int := 0; + c1 cursor for select * from forc_test; +begin + for r1 in c1 loop + declare c1 cursor for select * from forc_test; + begin + for r2 in c1 loop + cnt := cnt + 1; + end loop; + end; + end loop; + raise notice 'cnt = %', cnt; +end $$; +NOTICE: cnt = 100 -- fail because cursor has no query bound to it create or replace function forc_bad() returns void as $$ declare diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql index 588c331033..9a53b15081 100644 --- a/src/test/regress/sql/plpgsql.sql +++ b/src/test/regress/sql/plpgsql.sql @@ -2929,6 +2929,9 @@ declare c2 cursor for select * from generate_series(41,43) i; begin + -- assign portal names to cursors to get stable output + c := 'c'; + c2 := 'c2'; for r in c(5,7) loop raise notice '% from %', r.i, c; end loop; @@ -3002,6 +3005,23 @@ select * from forc_test; drop function forc01(); +-- it's okay to re-use a cursor variable name, even when bound + +do $$ +declare cnt int := 0; + c1 cursor for select * from forc_test; +begin + for r1 in c1 loop + declare c1 cursor for select * from forc_test; + begin + for r2 in c1 loop + cnt := cnt + 1; + end loop; + end; + end loop; + raise notice 'cnt = %', cnt; +end $$; + -- fail because cursor has no query bound to it create or replace function forc_bad() returns void as $$