Re: [REVIEW] Patch for cursor calling with named parameters

Started by Kevin Grittnerabout 14 years ago13 messages
#1Kevin Grittner
Kevin.Grittner@wicourts.gov

The patch is in context format, includes docs and additional
regression tests, applies cleanly, passes "world" regression tests.
The parser changes don't cause any "backing up".

Discussion on the list seems to have reached a consensus that this
patch implements a useful feature. A previous version was reviewed.
This version attempts to respond to points previously raised.

Overall, it looked good, but there were a few things I would like Yeb
to address before mark it is marked ready for committer. I don't
think any of these will take long.

(1) Doc changes:

(a) These contain some unnecessary whitespace changes which make it
harder to see exactly what changed.

(b) There's one point where "cursors" should be possessive
"cursor's".

(c) I think it would be less confusing to be consistent about
mentioning "positional" and "named" in the same order each
time. Mixing it up like that is harder to read, at least for
me.

(2) The error for mixing positional and named parameters should be
moved up. That seems more important than "too many arguments" or
"provided multiple times" if both are true.

(3) The "-- END ADDITIONAL TESTS" comment shouldn't be added to the
regression tests.

The way this parses out the named parameters and then builds the
positional list, with some code from read_sql_construct() repeated in
read_cursor_args() seems a little bit clumsy to me, but I couldn't
think of a better way to do it.

-Kevin

#2Yeb Havinga
yebhavinga@gmail.com
In reply to: Kevin Grittner (#1)
1 attachment(s)

Kevin,

Thank you for your review!

On 2011-12-03 21:53, Kevin Grittner wrote:

(1) Doc changes:

(a) These contain some unnecessary whitespace changes which make it
harder to see exactly what changed.

This is probably caused because I used emacs's fill-paragraph (alt+q)
command, after some changes. If this is against policy, I could change
the additions in such a way as to cause minor differences, however I
believe that the benefit of that ends immediately after review.

(b) There's one point where "cursors" should be possessive
"cursor's".

(c) I think it would be less confusing to be consistent about
mentioning "positional" and "named" in the same order each
time. Mixing it up like that is harder to read, at least for
me.

Good point, both changed.

(2) The error for mixing positional and named parameters should be
moved up. That seems more important than "too many arguments" or
"provided multiple times" if both are true.

I moved the error up, though I personally tend to believe it doesn't
even need to be an error. There is no technical reason not to allow it.
All the user needs to do is make sure that the combination of named
parameters and the positional ones together are complete and not
overlapping. This is also in line with calling functions, where mixing
named and positional is allowed, as long as the positional arguments are
first. Personally I have no opinion what is best, include the feature or
give an error, and I removed the possibility during the previous commitfest.

(3) The "-- END ADDITIONAL TESTS" comment shouldn't be added to the
regression tests.

This is removed, also the -- START ADDITIONAL TESTS, though I kept the
tests between those to comments.

The way this parses out the named parameters and then builds the
positional list, with some code from read_sql_construct() repeated in
read_cursor_args() seems a little bit clumsy to me, but I couldn't
think of a better way to do it.

Same here.

The new patch is attached.

regards
Yeb Havinga

Attachments:

cursornamedparameter-v5.patchtext/x-patch; name=cursornamedparameter-v5.patchDownload
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f33cef5..a3e6540
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
*************** OPEN curs1 FOR EXECUTE 'SELECT * FROM '
*** 2823,2833 ****
         </para>
       </sect3>
  
!     <sect3>
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
! OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional>;
  </synopsis>
  
           <para>
--- 2823,2833 ----
         </para>
       </sect3>
  
!     <sect3 id="plpgsql-open-bound-cursor">
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
!      OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional>;
  </synopsis>
  
           <para>
*************** OPEN <replaceable>bound_cursorvar</repla
*** 2847,2856 ****
--- 2847,2867 ----
           </para>
  
           <para>
+           Cursors may be opened using either <firstterm>positional</firstterm>
+           or <firstterm>named</firstterm> notation. In contrast with calling
+           functions, described in <xref linkend="sql-syntax-calling-funcs">, it
+           is not allowed to mix positional and named notation. In positional
+           notation, all arguments are specified in order. In named notation,
+           each argument's name is specified using <literal>:=</literal> to
+           separate it from the argument expression.
+          </para>
+ 
+          <para>
            Examples (these use the cursor declaration examples above):
  <programlisting>
  OPEN curs2;
  OPEN curs3(42);
+ OPEN curs3(key := 42);
  </programlisting>
           </para>
  
*************** COMMIT;
*** 3169,3186 ****
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
  
       The cursor variable must have been bound to some query when it was
!      declared, and it <emphasis>cannot</> be open already.  The
!      <command>FOR</> statement automatically opens the cursor, and it closes
!      the cursor again when the loop exits.  A list of actual argument value
!      expressions must appear if and only if the cursor was declared to take
!      arguments.  These values will be substituted in the query, in just
!      the same way as during an <command>OPEN</>.
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
--- 3180,3203 ----
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
  
       The cursor variable must have been bound to some query when it was
!      declared, and it <emphasis>cannot</> be open already.  The <command>FOR</>
!      statement automatically opens the cursor, and it closes the cursor again
!      when the loop exits.  The cursor's arguments may be supplied in either
!      <firstterm>positional</firstterm> or <firstterm>named</firstterm>
!      notation.  A list of actual argument value expressions must appear if and
!      only if the cursor was declared to take arguments.  These values will be
!      substituted in the query, in just the same way as during an
!      <command>OPEN</command> described in <xref
!      linkend="plpgsql-open-bound-cursor">.
!    </para>
! 
!    <para>
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
diff --git a/src/pl/plpgsql/src/gram.y b/src/pl/plpgsql/src/gram.y
new file mode 100644
index 8c4c2f7..f7d6b9d
*** a/src/pl/plpgsql/src/gram.y
--- b/src/pl/plpgsql/src/gram.y
*************** static	PLpgSQL_expr	*read_sql_construct(
*** 67,72 ****
--- 67,73 ----
  											const char *sqlstart,
  											bool isexpression,
  											bool valid_sql,
+ 											bool trim,
  											int *startloc,
  											int *endtoken);
  static	PLpgSQL_expr	*read_sql_expression(int until,
*************** for_control		: for_variable K_IN
*** 1313,1318 ****
--- 1314,1320 ----
  													   "SELECT ",
  													   true,
  													   false,
+ 													   true,
  													   &expr1loc,
  													   &tok);
  
*************** stmt_raise		: K_RAISE
*** 1692,1698 ****
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
--- 1694,1700 ----
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
*************** stmt_dynexecute : K_EXECUTE
*** 1790,1796 ****
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
--- 1792,1798 ----
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
*************** stmt_dynexecute : K_EXECUTE
*** 1829,1835 ****
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
--- 1831,1837 ----
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
*************** static PLpgSQL_expr *
*** 2322,2328 ****
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
--- 2324,2330 ----
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
*************** read_sql_expression2(int until, int unti
*** 2331,2337 ****
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
--- 2333,2339 ----
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
*************** static PLpgSQL_expr *
*** 2339,2345 ****
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, NULL, NULL);
  }
  
  /*
--- 2341,2347 ----
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, true, NULL, NULL);
  }
  
  /*
*************** read_sql_stmt(const char *sqlstart)
*** 2352,2357 ****
--- 2354,2360 ----
   * sqlstart:	text to prefix to the accumulated SQL text
   * isexpression: whether to say we're reading an "expression" or a "statement"
   * valid_sql:   whether to check the syntax of the expr (prefixed with sqlstart)
+  * bool:        trim trailing whitespace
   * startloc:	if not NULL, location of first token is stored at *startloc
   * endtoken:	if not NULL, ending token is stored at *endtoken
   *				(this is only interesting if until2 or until3 isn't zero)
*************** read_sql_construct(int until,
*** 2364,2369 ****
--- 2367,2373 ----
  				   const char *sqlstart,
  				   bool isexpression,
  				   bool valid_sql,
+ 				   bool trim,
  				   int *startloc,
  				   int *endtoken)
  {
*************** read_sql_construct(int until,
*** 2443,2450 ****
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 		ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
--- 2447,2455 ----
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	if (trim)
! 		while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 			ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
*************** check_labels(const char *start_label, co
*** 3374,3389 ****
  /*
   * Read the arguments (if any) for a cursor, followed by the until token
   *
!  * If cursor has no args, just swallow the until token and return NULL.
!  * If it does have args, we expect to see "( expr [, expr ...] )" followed
!  * by the until token.  Consume all that and return a SELECT query that
!  * evaluates the expression(s) (without the outer parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
! 	PLpgSQL_expr *expr;
! 	int			tok;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
--- 3379,3402 ----
  /*
   * Read the arguments (if any) for a cursor, followed by the until token
   *
!  * If cursor has no args, just swallow the until token and return NULL.  If it
!  * does have args, we expect to see "( expr [, expr ...] )" followed by the
!  * until token, where expr may be a plain expression, or a named parameter
!  * assignment of the form IDENT := expr. Consume all that and return a SELECT
!  * query that evaluates the expression(s) (without the outer parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
! 	PLpgSQL_expr    *expr;
! 	PLpgSQL_row     *row;
! 	int              tok;
! 	int              argc = 0;
! 	char           **argv;
! 	StringInfoData   ds;
! 	char            *sqlstart = "SELECT ";
! 	bool             positional = false;
! 	bool             named = false;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
*************** read_cursor_args(PLpgSQL_var *cursor, in
*** 3402,3407 ****
--- 3415,3423 ----
  		return NULL;
  	}
  
+ 	row = (PLpgSQL_row *) plpgsql_Datums[cursor->cursor_explicit_argrow];
+ 	argv = (char **) palloc0(sizeof(char *) * row->nfields);
+ 
  	/* Else better provide arguments */
  	if (tok != '(')
  		ereport(ERROR,
*************** read_cursor_args(PLpgSQL_var *cursor, in
*** 3410,3419 ****
  						cursor->refname),
  				 parser_errposition(yylloc)));
  
! 	/*
! 	 * Read expressions until the matching ')'.
! 	 */
! 	expr = read_sql_expression(')', ")");
  
  	/* Next we'd better find the until token */
  	tok = yylex();
--- 3426,3531 ----
  						cursor->refname),
  				 parser_errposition(yylloc)));
  
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		int argpos;
! 		int endtoken;
! 		PLpgSQL_expr *item;
! 		int arglocation;
! 
! 		if (plpgsql_isidentassign(&arglocation))
! 		{
! 			/* Named parameter assignment */
! 			named = true;
! 			for (argpos = 0; argpos < row->nfields; argpos++)
! 				if (strcmp(row->fieldnames[argpos], yylval.str) == 0)
! 					break;
! 
! 			if (argpos == row->nfields)
! 				ereport(ERROR,
! 						(errcode(ERRCODE_SYNTAX_ERROR),
! 						 errmsg("cursor \"%s\" has no argument named \"%s\"",
! 								cursor->refname, yylval.str),
! 						 parser_errposition(yylloc)));
! 		}
! 		else
! 		{
! 			/* Positional parameter assignment */
! 			positional = true;
! 			argpos = argc;
! 		}
! 
! 		/*
! 		 * Read one expression at a time until the matching endtoken. To
! 		 * provide the user with meaningful parse error positions, we check the
! 		 * syntax immediately, instead of checking the final expression that
! 		 * may have a permutated argument list. Also trailing whitespace may
! 		 * not be trimmed, since otherwise input of the form (param --
! 		 * comment\n, param) is translated into a form that comments the
! 		 * remaining parameters.
! 		 */
! 		item = 	read_sql_construct(',', ')', 0,
! 								   ",\" or \")",
! 								   sqlstart,
! 								   true, true,
! 								   false, /* do not trim */
! 								   NULL, &endtoken);
! 
! 		if (endtoken == ')' && !(argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("not enough arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (named && positional)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("mixing positional and named parameter assignment not allowed in cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(arglocation)));
! 
! 		if (endtoken == ',' && (argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("too many arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (argv[argpos] != NULL)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("cursor \"%s\" argument \"%s\" provided multiple times",
! 							cursor->refname, row->fieldnames[argpos]),
! 					 parser_errposition(arglocation)));
! 
! 		argv[argpos] = item->query + strlen(sqlstart);
! 	}
! 
! 	/* Make positional argument list */
! 	initStringInfo(&ds);
! 	appendStringInfoString(&ds, sqlstart);
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		Assert(argv[argc] != NULL);
! 
! 		/* Argument name included for meaningful runtime errors */
! 		if (named)
! 			appendStringInfo(&ds, "/* %s := */ ", row->fieldnames[argc]);
! 
! 		appendStringInfoString(&ds, argv[argc]);
! 		if (argc < row->nfields - 1)
! 			appendStringInfoString(&ds, ", ");
! 	}
! 	appendStringInfoChar(&ds, ';');
! 
! 	expr = palloc0(sizeof(PLpgSQL_expr));
! 	expr->dtype			= PLPGSQL_DTYPE_EXPR;
! 	expr->query			= pstrdup(ds.data);
! 	expr->plan			= NULL;
! 	expr->paramnos		= NULL;
! 	expr->ns            = plpgsql_ns_top();
! 	pfree(ds.data);
  
  	/* Next we'd better find the until token */
  	tok = yylex();
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
new file mode 100644
index 76e8436..ce3161c
*** a/src/pl/plpgsql/src/pl_scanner.c
--- b/src/pl/plpgsql/src/pl_scanner.c
*************** plpgsql_scanner_finish(void)
*** 583,585 ****
--- 583,622 ----
  	yyscanner = NULL;
  	scanorig = NULL;
  }
+ 
+ /*
+  * Return true if 'IDENT' ':=' are the next two tokens
+  *
+  * startloc:	if not NULL, location of first token is stored at *startloc
+  */
+ bool
+ plpgsql_isidentassign(int *startloc)
+ {
+ 	int          tok1, tok2;
+ 	TokenAuxData aux1, aux2;
+ 	bool         result = false;
+ 
+ 	tok1 = internal_yylex(&aux1);
+ 	if (startloc)
+ 		*startloc = aux1.lloc;
+ 
+ 	if (tok1 == IDENT)
+ 	{
+ 		tok2 = internal_yylex(&aux2);
+ 
+ 		if (tok2 == COLON_EQUALS)
+ 			result = true;
+ 		else
+ 			push_back_token(tok2, &aux2);
+ 	}
+ 
+ 	if (!result)
+ 		push_back_token(tok1, &aux1);
+ 
+ 	plpgsql_yylval = aux1.lval;
+ 	plpgsql_yylloc = aux1.lloc;
+ 	plpgsql_yyleng = aux1.leng;
+ 
+ 	return result;
+ }
+ 
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
new file mode 100644
index c638f43..b3ff847
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** extern int	plpgsql_location_to_lineno(in
*** 968,973 ****
--- 968,974 ----
  extern int	plpgsql_latest_lineno(void);
  extern void plpgsql_scanner_init(const char *str);
  extern void plpgsql_scanner_finish(void);
+ extern bool plpgsql_isidentassign(int *startloc);
  
  /* ----------
   * Externs in gram.y
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
new file mode 100644
index 238bf5f..8516466
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** select refcursor_test2(20000, 20000) as
*** 2292,2297 ****
--- 2292,2410 ----
  (1 row)
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, -- comment after , should be ignored
+             param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+  Should be false | Should be true 
+ -----------------+----------------
+  f               | t
+ (1 row)
+ 
+ -- may not mix named and positional, parse time error at $2
+ create function namedparmcursor_test2(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+     nonsense record;
+ begin
+     open c1(param1 := $1, $2);
+ end
+ $$ language plpgsql;
+ ERROR:  mixing positional and named parameter assignment not allowed in cursor "c1"
+ LINE 6:     open c1(param1 := $1, $2);
+                                   ^
+ -- not enough parameters, parse time error at );
+ create function namedparmcursor_test6(int, int) returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $2);
+ end
+ $$ language plpgsql;
+ ERROR:  not enough arguments for cursor "c1"
+ LINE 5:     open c1(param2 := $2);
+                                 ^
+ -- multiple same parameter, parse time error at second p2
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end $$ language plpgsql;
+ ERROR:  cursor "c1" argument "p2" provided multiple times
+ LINE 6:   open c1 (p2 := 77, p2 := 42);
+                              ^
+ -- division by zero runtime error,
+ -- provides context users can make sense of:
+ -- CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select fooey();
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ PL/pgSQL function "fooey" line 6 at OPEN
+ -- test comment and newline structure, will not give runtime error when
+ -- read_sql_construct trims trailing whitespace
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (77 -- test
+   , 42/);
+ end $$ language plpgsql;
+ ERROR:  syntax error at end of input
+ LINE 7:   , 42/);
+                ^
+ select fooey();
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ PL/pgSQL function "fooey" line 6 at OPEN
+ -- test syntax error at :
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42, p1 : = 77);
+ end $$ language plpgsql;
+ ERROR:  syntax error at or near ":"
+ LINE 6:   open c1 ( p2 := 42, p1 : = 77);
+                                  ^
+ -- test another syntax error at ,
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42/, p1 : = 77);
+ end $$ language plpgsql;
+ ERROR:  syntax error at end of input
+ LINE 6:   open c1 ( p2 := 42/, p1 : = 77);
+                              ^
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
new file mode 100644
index b47c2de..749aa46
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** select refcursor_test2(20000, 20000) as
*** 1946,1951 ****
--- 1946,2044 ----
         refcursor_test2(20, 20) as "Should be true";
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, -- comment after , should be ignored
+             param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ 
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+ 
+ -- may not mix named and positional, parse time error at $2
+ create function namedparmcursor_test2(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+     nonsense record;
+ begin
+     open c1(param1 := $1, $2);
+ end
+ $$ language plpgsql;
+ 
+ -- not enough parameters, parse time error at );
+ create function namedparmcursor_test6(int, int) returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $2);
+ end
+ $$ language plpgsql;
+ 
+ -- multiple same parameter, parse time error at second p2
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end $$ language plpgsql;
+ 
+ -- division by zero runtime error,
+ -- provides context users can make sense of:
+ -- CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select fooey();
+ 
+ -- test comment and newline structure, will not give runtime error when
+ -- read_sql_construct trims trailing whitespace
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (77 -- test
+   , 42/);
+ end $$ language plpgsql;
+ select fooey();
+ 
+ -- test syntax error at :
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42, p1 : = 77);
+ end $$ language plpgsql;
+ 
+ -- test another syntax error at ,
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42/, p1 : = 77);
+ end $$ language plpgsql;
+ 
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
#3Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Yeb Havinga (#2)

Yeb Havinga <yebhavinga@gmail.com> wrote:

On 2011-12-03 21:53, Kevin Grittner wrote:

(1) Doc changes:

(a) These contain some unnecessary whitespace changes which
make it harder to see exactly what changed.

This is probably caused because I used emacs's fill-paragraph
(alt+q) command, after some changes. If this is against policy, I
could change the additions in such a way as to cause minor
differences, however I believe that the benefit of that ends
immediately after review.

I don't know whether it's a hard policy, but people usually minimize
whitespace changes in such situations, to make it easier for the
reviewer and committer to tell what really changed. The committer
can always reformat after looking that over, if they feel that's
useful. The issue is small enough here that I'm not inclined to
consider it a blocker for sending to the committer.

(2) The error for mixing positional and named parameters should
be moved up. That seems more important than "too many arguments"
or "provided multiple times" if both are true.

I moved the error up, though I personally tend to believe it
doesn't even need to be an error. There is no technical reason not
to allow it. All the user needs to do is make sure that the
combination of named parameters and the positional ones together
are complete and not overlapping. This is also in line with
calling functions, where mixing named and positional is allowed,
as long as the positional arguments are first. Personally I have
no opinion what is best, include the feature or give an error, and
I removed the possibility during the previous commitfest.

If there's no technical reason not to allow them to be mixed, I
would tend to favor consistency with parameter calling rules. Doing
otherwise seems like to result in confusion and "bug" reports.

How do others feel?

-Kevin

#4Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Kevin Grittner (#3)
Re: [REVIEW] Patch for cursor calling with named parameters

"Kevin Grittner" <Kevin.Grittner@wicourts.gov> wrote:

Yeb Havinga <yebhavinga@gmail.com> wrote:

I personally tend to believe it doesn't even need to be an error.
There is no technical reason not to allow it. All the user needs
to do is make sure that the combination of named parameters and
the positional ones together are complete and not overlapping.
This is also in line with calling functions, where mixing named
and positional is allowed, as long as the positional arguments
are first. Personally I have no opinion what is best, include the
feature or give an error, and I removed the possibility during
the previous commitfest.

If there's no technical reason not to allow them to be mixed, I
would tend to favor consistency with parameter calling rules.
Doing otherwise seems like to result in confusion and "bug"
reports.

Er, that was supposed to read "I would tend to favor consistency
with function calling rules." As stated here:

http://www.postgresql.org/docs/9.1/interactive/sql-syntax-calling-funcs.html

| PostgreSQL also supports mixed notation, which combines positional
| and named notation. In this case, positional parameters are
| written first and named parameters appear after them.

How do others feel?

If there are no objections, I suggest that Yeb implement the mixed
notation for cursor parameters.

-Kevin

#5Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Kevin Grittner (#1)
Re: [REVIEW] Patch for cursor calling with named parameters

Kevin Grittner <kgrittn@wicourts.gov> wrote:

Yeb Havinga <yebhavinga@gmail.com> wrote:

I personally tend to believe it doesn't even need to be an error.
There is no technical reason not to allow it. All the user needs
to do is make sure that the combination of named parameters and
the positional ones together are complete and not overlapping.

If there are no objections, I suggest that Yeb implement the mixed
notation for cursor parameters.

Hearing no objections -- Yeb, are you OK with doing this, and do you
feel this is doable for this CF?

-Kevin

#6Yeb Havinga
yebhavinga@gmail.com
In reply to: Kevin Grittner (#5)

On 2011-12-06 17:58, Kevin Grittner wrote:

Kevin Grittner<kgrittn@wicourts.gov> wrote:

If there are no objections, I suggest that Yeb implement the mixed
notation for cursor parameters.

Hearing no objections -- Yeb, are you OK with doing this, and do you
feel this is doable for this CF?

It is not a large change, I'll be able to do it tomorrow if nothing
unexpected happens, or monday otherwise.

regards,
Yeb

#7Yeb Havinga
yebhavinga@gmail.com
In reply to: Kevin Grittner (#5)

On 2011-12-06 17:58, Kevin Grittner wrote:

Kevin Grittner<kgrittn@wicourts.gov> wrote:

Yeb Havinga<yebhavinga@gmail.com> wrote:

I personally tend to believe it doesn't even need to be an error.
There is no technical reason not to allow it. All the user needs
to do is make sure that the combination of named parameters and
the positional ones together are complete and not overlapping.

If there are no objections, I suggest that Yeb implement the mixed
notation for cursor parameters.

Hearing no objections -- Yeb, are you OK with doing this, and do you
feel this is doable for this CF?

Attach is v6 of the patch, allowing mixed mode and with new test cases
in the regression tests. One difference with calling functions remains:
it is allowed to place positional arguments after named parameters.
Including that would add code, but nothing would be gained.

regards,
Yeb Havinga

#8Yeb Havinga
yebhavinga@gmail.com
In reply to: Yeb Havinga (#7)
1 attachment(s)

On 2011-12-11 16:26, Yeb Havinga wrote:

On 2011-12-06 17:58, Kevin Grittner wrote:

Kevin Grittner<kgrittn@wicourts.gov> wrote:

Yeb Havinga<yebhavinga@gmail.com> wrote:

I personally tend to believe it doesn't even need to be an error.
There is no technical reason not to allow it. All the user needs
to do is make sure that the combination of named parameters and
the positional ones together are complete and not overlapping.

If there are no objections, I suggest that Yeb implement the mixed
notation for cursor parameters.

Hearing no objections -- Yeb, are you OK with doing this, and do you
feel this is doable for this CF?

Attach is v6 of the patch, allowing mixed mode and with new test cases
in the regression tests. One difference with calling functions
remains: it is allowed to place positional arguments after named
parameters. Including that would add code, but nothing would be gained.

Forgot to copy regression output to expected - attached v7 fixes that.

-- Yeb

Attachments:

cursornamedparameter-v7.patchtext/x-patch; name=cursornamedparameter-v7.patchDownload
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index f33cef5..ee2e3a3
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
*************** OPEN curs1 FOR EXECUTE 'SELECT * FROM '
*** 2823,2833 ****
         </para>
       </sect3>
  
!     <sect3>
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
! OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional>;
  </synopsis>
  
           <para>
--- 2823,2833 ----
         </para>
       </sect3>
  
!     <sect3 id="plpgsql-open-bound-cursor">
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
!      OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional>;
  </synopsis>
  
           <para>
*************** OPEN <replaceable>bound_cursorvar</repla
*** 2847,2856 ****
--- 2847,2867 ----
           </para>
  
           <para>
+           Cursors may be opened using either <firstterm>positional</firstterm>
+           or <firstterm>named</firstterm> notation.  Similar to calling
+           functions, described in <xref linkend="sql-syntax-calling-funcs">, it
+           is also allowed to mix positional and named notation.  In positional
+           notation, all arguments are specified in order.  In named notation,
+           each argument's name is specified using <literal>:=</literal> to
+           separate it from the argument expression.
+          </para>
+ 
+          <para>
            Examples (these use the cursor declaration examples above):
  <programlisting>
  OPEN curs2;
  OPEN curs3(42);
+ OPEN curs3(key := 42);
  </programlisting>
           </para>
  
*************** COMMIT;
*** 3169,3186 ****
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
  
       The cursor variable must have been bound to some query when it was
!      declared, and it <emphasis>cannot</> be open already.  The
!      <command>FOR</> statement automatically opens the cursor, and it closes
!      the cursor again when the loop exits.  A list of actual argument value
!      expressions must appear if and only if the cursor was declared to take
!      arguments.  These values will be substituted in the query, in just
!      the same way as during an <command>OPEN</>.
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
--- 3180,3203 ----
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
  
       The cursor variable must have been bound to some query when it was
!      declared, and it <emphasis>cannot</> be open already.  The <command>FOR</>
!      statement automatically opens the cursor, and it closes the cursor again
!      when the loop exits.  The cursor's arguments may be supplied in either
!      <firstterm>positional</firstterm> or <firstterm>named</firstterm>
!      notation.  A list of actual argument value expressions must appear if and
!      only if the cursor was declared to take arguments.  These values will be
!      substituted in the query, in just the same way as during an
!      <command>OPEN</command> described in <xref
!      linkend="plpgsql-open-bound-cursor">.
!    </para>
! 
!    <para>
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
diff --git a/src/pl/plpgsql/src/gram.y b/src/pl/plpgsql/src/gram.y
new file mode 100644
index 8c4c2f7..107007a
*** a/src/pl/plpgsql/src/gram.y
--- b/src/pl/plpgsql/src/gram.y
*************** static	PLpgSQL_expr	*read_sql_construct(
*** 67,72 ****
--- 67,73 ----
  											const char *sqlstart,
  											bool isexpression,
  											bool valid_sql,
+ 											bool trim,
  											int *startloc,
  											int *endtoken);
  static	PLpgSQL_expr	*read_sql_expression(int until,
*************** for_control		: for_variable K_IN
*** 1313,1318 ****
--- 1314,1320 ----
  													   "SELECT ",
  													   true,
  													   false,
+ 													   true,
  													   &expr1loc,
  													   &tok);
  
*************** stmt_raise		: K_RAISE
*** 1692,1698 ****
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
--- 1694,1700 ----
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
*************** stmt_dynexecute : K_EXECUTE
*** 1790,1796 ****
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
--- 1792,1798 ----
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
*************** stmt_dynexecute : K_EXECUTE
*** 1829,1835 ****
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
--- 1831,1837 ----
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
*************** static PLpgSQL_expr *
*** 2322,2328 ****
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
--- 2324,2330 ----
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
*************** read_sql_expression2(int until, int unti
*** 2331,2337 ****
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
--- 2333,2339 ----
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
*************** static PLpgSQL_expr *
*** 2339,2345 ****
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, NULL, NULL);
  }
  
  /*
--- 2341,2347 ----
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, true, NULL, NULL);
  }
  
  /*
*************** read_sql_stmt(const char *sqlstart)
*** 2352,2357 ****
--- 2354,2360 ----
   * sqlstart:	text to prefix to the accumulated SQL text
   * isexpression: whether to say we're reading an "expression" or a "statement"
   * valid_sql:   whether to check the syntax of the expr (prefixed with sqlstart)
+  * bool:        trim trailing whitespace
   * startloc:	if not NULL, location of first token is stored at *startloc
   * endtoken:	if not NULL, ending token is stored at *endtoken
   *				(this is only interesting if until2 or until3 isn't zero)
*************** read_sql_construct(int until,
*** 2364,2369 ****
--- 2367,2373 ----
  				   const char *sqlstart,
  				   bool isexpression,
  				   bool valid_sql,
+ 				   bool trim,
  				   int *startloc,
  				   int *endtoken)
  {
*************** read_sql_construct(int until,
*** 2443,2450 ****
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 		ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
--- 2447,2455 ----
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	if (trim)
! 		while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 			ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
*************** check_labels(const char *start_label, co
*** 3374,3389 ****
  /*
   * Read the arguments (if any) for a cursor, followed by the until token
   *
!  * If cursor has no args, just swallow the until token and return NULL.
!  * If it does have args, we expect to see "( expr [, expr ...] )" followed
!  * by the until token.  Consume all that and return a SELECT query that
!  * evaluates the expression(s) (without the outer parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
! 	PLpgSQL_expr *expr;
! 	int			tok;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
--- 3379,3401 ----
  /*
   * Read the arguments (if any) for a cursor, followed by the until token
   *
!  * If cursor has no args, just swallow the until token and return NULL.  If it
!  * does have args, we expect to see "( expr [, expr ...] )" followed by the
!  * until token, where expr may be a plain expression, or a named parameter
!  * assignment of the form IDENT := expr. Consume all that and return a SELECT
!  * query that evaluates the expression(s) (without the outer parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
! 	PLpgSQL_expr    *expr;
! 	PLpgSQL_row     *row;
! 	int              tok;
! 	int              argc = 0;
! 	char           **argv;
! 	StringInfoData   ds;
! 	char            *sqlstart = "SELECT ";
! 	bool             named = false;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
*************** read_cursor_args(PLpgSQL_var *cursor, in
*** 3402,3407 ****
--- 3414,3422 ----
  		return NULL;
  	}
  
+ 	row = (PLpgSQL_row *) plpgsql_Datums[cursor->cursor_explicit_argrow];
+ 	argv = (char **) palloc0(sizeof(char *) * row->nfields);
+ 
  	/* Else better provide arguments */
  	if (tok != '(')
  		ereport(ERROR,
*************** read_cursor_args(PLpgSQL_var *cursor, in
*** 3410,3419 ****
  						cursor->refname),
  				 parser_errposition(yylloc)));
  
! 	/*
! 	 * Read expressions until the matching ')'.
! 	 */
! 	expr = read_sql_expression(')', ")");
  
  	/* Next we'd better find the until token */
  	tok = yylex();
--- 3425,3525 ----
  						cursor->refname),
  				 parser_errposition(yylloc)));
  
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		int argpos;
! 		int endtoken;
! 		PLpgSQL_expr *item;
! 		int arglocation;
! 
! 		if (plpgsql_isidentassign(&arglocation))
! 		{
! 			/* Named parameter assignment */
! 			named = true;
! 			for (argpos = 0; argpos < row->nfields; argpos++)
! 				if (strcmp(row->fieldnames[argpos], yylval.str) == 0)
! 					break;
! 
! 			if (argpos == row->nfields)
! 				ereport(ERROR,
! 						(errcode(ERRCODE_SYNTAX_ERROR),
! 						 errmsg("cursor \"%s\" has no argument named \"%s\"",
! 								cursor->refname, yylval.str),
! 						 parser_errposition(yylloc)));
! 		}
! 		else
! 		{
! 			/* Positional parameter assignment */
! 			argpos = argc;
! 		}
! 
! 		/*
! 		 * Read one expression at a time until the matching endtoken. To
! 		 * provide the user with meaningful parse error positions, we check the
! 		 * syntax immediately, instead of checking the final expression that
! 		 * may have a permutated argument list. Also trailing whitespace may
! 		 * not be trimmed, since otherwise input of the form (param --
! 		 * comment\n, param) is translated into a form that comments the
! 		 * remaining parameters.
! 		 */
! 		item = 	read_sql_construct(',', ')', 0,
! 								   ",\" or \")",
! 								   sqlstart,
! 								   true, true,
! 								   false, /* do not trim */
! 								   NULL, &endtoken);
! 
! 		if (endtoken == ')' && !(argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("not enough arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (endtoken == ',' && (argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("too many arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (argv[argpos] != NULL)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("duplicate value for cursor \"%s\" parameter \"%s\"",
! 							cursor->refname, row->fieldnames[argpos]),
! 					 parser_errposition(arglocation)));
! 
! 		argv[argpos] = item->query + strlen(sqlstart);
! 	}
! 
! 	/* Make positional argument list */
! 	initStringInfo(&ds);
! 	appendStringInfoString(&ds, sqlstart);
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		Assert(argv[argc] != NULL);
! 
! 		/*
! 		 * Because named notation allows permutated argument lists, include
! 		 * the parameter name for meaningful runtime errors.
! 		 */
! 		if (named)
! 			appendStringInfo(&ds, "/* %s := */ ", row->fieldnames[argc]);
! 
! 		appendStringInfoString(&ds, argv[argc]);
! 		if (argc < row->nfields - 1)
! 			appendStringInfoString(&ds, ", ");
! 	}
! 	appendStringInfoChar(&ds, ';');
! 
! 	expr = palloc0(sizeof(PLpgSQL_expr));
! 	expr->dtype			= PLPGSQL_DTYPE_EXPR;
! 	expr->query			= pstrdup(ds.data);
! 	expr->plan			= NULL;
! 	expr->paramnos		= NULL;
! 	expr->ns            = plpgsql_ns_top();
! 	pfree(ds.data);
  
  	/* Next we'd better find the until token */
  	tok = yylex();
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
new file mode 100644
index 76e8436..ce3161c
*** a/src/pl/plpgsql/src/pl_scanner.c
--- b/src/pl/plpgsql/src/pl_scanner.c
*************** plpgsql_scanner_finish(void)
*** 583,585 ****
--- 583,622 ----
  	yyscanner = NULL;
  	scanorig = NULL;
  }
+ 
+ /*
+  * Return true if 'IDENT' ':=' are the next two tokens
+  *
+  * startloc:	if not NULL, location of first token is stored at *startloc
+  */
+ bool
+ plpgsql_isidentassign(int *startloc)
+ {
+ 	int          tok1, tok2;
+ 	TokenAuxData aux1, aux2;
+ 	bool         result = false;
+ 
+ 	tok1 = internal_yylex(&aux1);
+ 	if (startloc)
+ 		*startloc = aux1.lloc;
+ 
+ 	if (tok1 == IDENT)
+ 	{
+ 		tok2 = internal_yylex(&aux2);
+ 
+ 		if (tok2 == COLON_EQUALS)
+ 			result = true;
+ 		else
+ 			push_back_token(tok2, &aux2);
+ 	}
+ 
+ 	if (!result)
+ 		push_back_token(tok1, &aux1);
+ 
+ 	plpgsql_yylval = aux1.lval;
+ 	plpgsql_yylloc = aux1.lloc;
+ 	plpgsql_yyleng = aux1.leng;
+ 
+ 	return result;
+ }
+ 
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
new file mode 100644
index c638f43..b3ff847
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** extern int	plpgsql_location_to_lineno(in
*** 968,973 ****
--- 968,974 ----
  extern int	plpgsql_latest_lineno(void);
  extern void plpgsql_scanner_init(const char *str);
  extern void plpgsql_scanner_finish(void);
+ extern bool plpgsql_isidentassign(int *startloc);
  
  /* ----------
   * Externs in gram.y
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
new file mode 100644
index 238bf5f..1df0f23
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** select refcursor_test2(20000, 20000) as
*** 2292,2297 ****
--- 2292,2428 ----
  (1 row)
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, -- comment after , should be ignored
+             param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+  Should be false | Should be true 
+ -----------------+----------------
+  f               | t
+ (1 row)
+ 
+ -- mixing named and positional, ok
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param1 := $1, $2);
+ end
+ $$ language plpgsql;
+ -- mixing named and positional, double param2, parse time error at second value
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $1, $2);
+ end
+ $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "param2"
+ LINE 5:     open c1(param2 := $1, $2);
+                                   ^
+ -- mixing named and positional, double param1, parse time error at second value
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1($1, param1 := $2);
+ end
+ $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "param1"
+ LINE 5:     open c1($1, param1 := $2);
+                         ^
+ -- not enough parameters, parse time error at );
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $2);
+ end
+ $$ language plpgsql;
+ ERROR:  not enough arguments for cursor "c1"
+ LINE 5:     open c1(param2 := $2);
+                                 ^
+ -- double parameter, parse time error at second p2
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "p2"
+ LINE 6:   open c1 (p2 := 77, p2 := 42);
+                              ^
+ -- division by zero runtime error,
+ -- provides context users can make sense of:
+ -- CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select fooey();
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ PL/pgSQL function "fooey" line 6 at OPEN
+ -- test comment and newline structure, will not give runtime error when
+ -- read_sql_construct trims trailing whitespace
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (77 -- test
+   , 42/);
+ end $$ language plpgsql;
+ ERROR:  syntax error at end of input
+ LINE 7:   , 42/);
+                ^
+ select fooey();
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ PL/pgSQL function "fooey" line 6 at OPEN
+ -- test syntax error at :
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42, p1 : = 77);
+ end $$ language plpgsql;
+ ERROR:  syntax error at or near ":"
+ LINE 6:   open c1 ( p2 := 42, p1 : = 77);
+                                  ^
+ -- test another syntax error at ,
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42/, p1 : = 77);
+ end $$ language plpgsql;
+ ERROR:  syntax error at end of input
+ LINE 6:   open c1 ( p2 := 42/, p1 : = 77);
+                              ^
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
new file mode 100644
index b47c2de..f1fb7fd
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** select refcursor_test2(20000, 20000) as
*** 1946,1951 ****
--- 1946,2061 ----
         refcursor_test2(20, 20) as "Should be true";
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, -- comment after , should be ignored
+             param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ 
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+ 
+ -- mixing named and positional, ok
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param1 := $1, $2);
+ end
+ $$ language plpgsql;
+ 
+ -- mixing named and positional, double param2, parse time error at second value
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $1, $2);
+ end
+ $$ language plpgsql;
+ 
+ -- mixing named and positional, double param1, parse time error at second value
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1($1, param1 := $2);
+ end
+ $$ language plpgsql;
+ 
+ -- not enough parameters, parse time error at );
+ create or replace function named(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := $2);
+ end
+ $$ language plpgsql;
+ 
+ -- double parameter, parse time error at second p2
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end $$ language plpgsql;
+ 
+ -- division by zero runtime error,
+ -- provides context users can make sense of:
+ -- CONTEXT:  SQL statement "SELECT /* p1 := */ 42/0, /* p2 := */ 77;"
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select fooey();
+ 
+ -- test comment and newline structure, will not give runtime error when
+ -- read_sql_construct trims trailing whitespace
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (77 -- test
+   , 42/);
+ end $$ language plpgsql;
+ select fooey();
+ 
+ -- test syntax error at :
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42, p1 : = 77);
+ end $$ language plpgsql;
+ 
+ -- test another syntax error at ,
+ create or replace function fooey() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 ( p2 := 42/, p1 : = 77);
+ end $$ language plpgsql;
+ 
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
#9Kevin Grittner
Kevin.Grittner@wicourts.gov
In reply to: Kevin Grittner (#5)
Re: [REVIEW] Patch for cursor calling with named parameters

Yeb Havinga wrote:

Forgot to copy regression output to expected - attached v7 fixes
that.

This version addresses all of my concerns. It applies cleanly and
compiles without warning against current HEAD and performs as
advertised. I'm marking it Ready for Committer.

-Kevin

#10Heikki Linnakangas
heikki.linnakangas@enterprisedb.com
In reply to: Kevin Grittner (#9)
1 attachment(s)

On 12.12.2011 21:55, Kevin Grittner wrote:

Yeb Havinga wrote:

Forgot to copy regression output to expected - attached v7 fixes
that.

This version addresses all of my concerns. It applies cleanly and
compiles without warning against current HEAD and performs as
advertised. I'm marking it Ready for Committer.

This failed:

postgres=# do $$
declare
foocur CURSOR ("insane /* arg" int4) IS SELECT generate_series(1,
"insane /* arg");
begin
OPEN foocur("insane /* arg" := 10);
end;
$$;
ERROR: unterminated /* comment at or near "/* insane /* arg := */ 10;"
LINE 1: SELECT /* insane /* arg := */ 10;
^
QUERY: SELECT /* insane /* arg := */ 10;
CONTEXT: PL/pgSQL function "inline_code_block" line 5 at OPEN

I don't have much sympathy for anyone who uses argument names like that,
but it nevertheless ought to not fail. A simple way to fix that is to
constuct the query as: "value AS argname", instead of "/* argname := */
value". Then you can use the existing quote_identifier() function to do
the necessary quoting.

I replaced the plpgsql_isidentassign() function with a more generic
plpgsql_peek2() function, which allows you to peek ahead two tokens in
the input stream, without eating them. It's implemented using the
pushdown stack like plpgsql_isidentassign() was, but the division of
labor between pl_scanner.c and gram.y seems more clear this way. I'm
still not 100% happy with it. plpgsql_peek2() behaves differently from
plpgsql_yylex(), in that it doesn't perform variable or unreserved
keyword lookup. It could do that, but it would be quite pointless since
the only current caller doesn't want variable or unreserved keyword
lookup, so it would just have to work harder to undo them.

Attached is a patch with those changes. I also I removed a few of the
syntax error regression tests, that seemed excessive, plus some general
naming and comment fiddling. I'll apply this tomorrow, if it still looks
good to me after sleeping on it.

--
Heikki Linnakangas
EnterpriseDB http://www.enterprisedb.com

Attachments:

cursornamedparameter-v8-heikki.patchtext/x-diff; name=cursornamedparameter-v8-heikki.patchDownload
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
***************
*** 2823,2833 **** OPEN curs1 FOR EXECUTE 'SELECT * FROM ' || quote_ident(tabname)
         </para>
       </sect3>
  
!     <sect3>
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
! OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional>;
  </synopsis>
  
           <para>
--- 2823,2833 ----
         </para>
       </sect3>
  
!     <sect3 id="plpgsql-open-bound-cursor">
       <title>Opening a Bound Cursor</title>
  
  <synopsis>
! OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional>;
  </synopsis>
  
           <para>
***************
*** 2847,2856 **** OPEN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argume
--- 2847,2867 ----
           </para>
  
           <para>
+           Argument values can be passed using either <firstterm>positional</firstterm>
+           or <firstterm>named</firstterm> notation.  In positional
+           notation, all arguments are specified in order.  In named notation,
+           each argument's name is specified using <literal>:=</literal> to
+           separate it from the argument expression. Similar to calling
+           functions, described in <xref linkend="sql-syntax-calling-funcs">, it
+           is also allowed to mix positional and named notation.
+          </para>
+ 
+          <para>
            Examples (these use the cursor declaration examples above):
  <programlisting>
  OPEN curs2;
  OPEN curs3(42);
+ OPEN curs3(key := 42);
  </programlisting>
           </para>
  
***************
*** 3169,3175 **** COMMIT;
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <replaceable>argument_values</replaceable> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
--- 3180,3186 ----
  
  <synopsis>
  <optional> &lt;&lt;<replaceable>label</replaceable>&gt;&gt; </optional>
! FOR <replaceable>recordvar</replaceable> IN <replaceable>bound_cursorvar</replaceable> <optional> ( <optional> <replaceable>argname</replaceable> := </optional> <replaceable>argument_value</replaceable> <optional>, ...</optional> ) </optional> LOOP
      <replaceable>statements</replaceable>
  END LOOP <optional> <replaceable>label</replaceable> </optional>;
  </synopsis>
***************
*** 3179,3186 **** END LOOP <optional> <replaceable>label</replaceable> </optional>;
       <command>FOR</> statement automatically opens the cursor, and it closes
       the cursor again when the loop exits.  A list of actual argument value
       expressions must appear if and only if the cursor was declared to take
!      arguments.  These values will be substituted in the query, in just
!      the same way as during an <command>OPEN</>.
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
--- 3190,3201 ----
       <command>FOR</> statement automatically opens the cursor, and it closes
       the cursor again when the loop exits.  A list of actual argument value
       expressions must appear if and only if the cursor was declared to take
!      arguments. These values will be substituted in the query, in just
!      the same way as during an <command>OPEN</> (see <xref
!      linkend="plpgsql-open-bound-cursor">).
!    </para>
! 
!    <para>
       The variable <replaceable>recordvar</replaceable> is automatically
       defined as type <type>record</> and exists only inside the loop (any
       existing definition of the variable name is ignored within the loop).
*** a/src/pl/plpgsql/src/gram.y
--- b/src/pl/plpgsql/src/gram.y
***************
*** 67,72 **** static	PLpgSQL_expr	*read_sql_construct(int until,
--- 67,73 ----
  											const char *sqlstart,
  											bool isexpression,
  											bool valid_sql,
+ 											bool trim,
  											int *startloc,
  											int *endtoken);
  static	PLpgSQL_expr	*read_sql_expression(int until,
***************
*** 1313,1318 **** for_control		: for_variable K_IN
--- 1314,1320 ----
  													   "SELECT ",
  													   true,
  													   false,
+ 													   true,
  													   &expr1loc,
  													   &tok);
  
***************
*** 1692,1698 **** stmt_raise		: K_RAISE
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
--- 1694,1700 ----
  									expr = read_sql_construct(',', ';', K_USING,
  															  ", or ; or USING",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &tok);
  									new->params = lappend(new->params, expr);
  								}
***************
*** 1790,1796 **** stmt_dynexecute : K_EXECUTE
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
--- 1792,1798 ----
  						expr = read_sql_construct(K_INTO, K_USING, ';',
  												  "INTO or USING or ;",
  												  "SELECT ",
! 												  true, true, true,
  												  NULL, &endtoken);
  
  						new = palloc(sizeof(PLpgSQL_stmt_dynexecute));
***************
*** 1829,1835 **** stmt_dynexecute : K_EXECUTE
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
--- 1831,1837 ----
  									expr = read_sql_construct(',', ';', K_INTO,
  															  ", or ; or INTO",
  															  "SELECT ",
! 															  true, true, true,
  															  NULL, &endtoken);
  									new->params = lappend(new->params, expr);
  								} while (endtoken == ',');
***************
*** 2322,2328 **** static PLpgSQL_expr *
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
--- 2324,2330 ----
  read_sql_expression(int until, const char *expected)
  {
  	return read_sql_construct(until, 0, 0, expected,
! 							  "SELECT ", true, true, true, NULL, NULL);
  }
  
  /* Convenience routine to read an expression with two possible terminators */
***************
*** 2331,2337 **** read_sql_expression2(int until, int until2, const char *expected,
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
--- 2333,2339 ----
  					 int *endtoken)
  {
  	return read_sql_construct(until, until2, 0, expected,
! 							  "SELECT ", true, true, true, NULL, endtoken);
  }
  
  /* Convenience routine to read a SQL statement that must end with ';' */
***************
*** 2339,2345 **** static PLpgSQL_expr *
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, NULL, NULL);
  }
  
  /*
--- 2341,2347 ----
  read_sql_stmt(const char *sqlstart)
  {
  	return read_sql_construct(';', 0, 0, ";",
! 							  sqlstart, false, true, true, NULL, NULL);
  }
  
  /*
***************
*** 2352,2357 **** read_sql_stmt(const char *sqlstart)
--- 2354,2360 ----
   * sqlstart:	text to prefix to the accumulated SQL text
   * isexpression: whether to say we're reading an "expression" or a "statement"
   * valid_sql:   whether to check the syntax of the expr (prefixed with sqlstart)
+  * bool:        trim trailing whitespace
   * startloc:	if not NULL, location of first token is stored at *startloc
   * endtoken:	if not NULL, ending token is stored at *endtoken
   *				(this is only interesting if until2 or until3 isn't zero)
***************
*** 2364,2369 **** read_sql_construct(int until,
--- 2367,2373 ----
  				   const char *sqlstart,
  				   bool isexpression,
  				   bool valid_sql,
+ 				   bool trim,
  				   int *startloc,
  				   int *endtoken)
  {
***************
*** 2443,2450 **** read_sql_construct(int until,
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 		ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
--- 2447,2455 ----
  	plpgsql_append_source_text(&ds, startlocation, yylloc);
  
  	/* trim any trailing whitespace, for neatness */
! 	if (trim)
! 		while (ds.len > 0 && scanner_isspace(ds.data[ds.len - 1]))
! 			ds.data[--ds.len] = '\0';
  
  	expr = palloc0(sizeof(PLpgSQL_expr));
  	expr->dtype			= PLPGSQL_DTYPE_EXPR;
***************
*** 3376,3389 **** check_labels(const char *start_label, const char *end_label, int end_location)
   *
   * If cursor has no args, just swallow the until token and return NULL.
   * If it does have args, we expect to see "( expr [, expr ...] )" followed
!  * by the until token.  Consume all that and return a SELECT query that
!  * evaluates the expression(s) (without the outer parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
  	PLpgSQL_expr *expr;
  	int			tok;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
--- 3381,3402 ----
   *
   * If cursor has no args, just swallow the until token and return NULL.
   * If it does have args, we expect to see "( expr [, expr ...] )" followed
!  * by the until token, where expr may be a plain expression, or a named
!  * parameter assignment of the form IDENT := expr. Consume all that and
!  * return a SELECT query that evaluates the expression(s) (without the outer
!  * parens).
   */
  static PLpgSQL_expr *
  read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  {
  	PLpgSQL_expr *expr;
+ 	PLpgSQL_row *row;
  	int			tok;
+ 	int			argc = 0;
+ 	char	  **argv;
+ 	StringInfoData ds;
+ 	char	   *sqlstart = "SELECT ";
+ 	bool		named = false;
  
  	tok = yylex();
  	if (cursor->cursor_explicit_argrow < 0)
***************
*** 3402,3407 **** read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
--- 3415,3423 ----
  		return NULL;
  	}
  
+ 	row = (PLpgSQL_row *) plpgsql_Datums[cursor->cursor_explicit_argrow];
+ 	argv = (char **) palloc0(row->nfields * sizeof(char *));
+ 
  	/* Else better provide arguments */
  	if (tok != '(')
  		ereport(ERROR,
***************
*** 3411,3419 **** read_cursor_args(PLpgSQL_var *cursor, int until, const char *expected)
  				 parser_errposition(yylloc)));
  
  	/*
! 	 * Read expressions until the matching ')'.
  	 */
! 	expr = read_sql_expression(')', ")");
  
  	/* Next we'd better find the until token */
  	tok = yylex();
--- 3427,3545 ----
  				 parser_errposition(yylloc)));
  
  	/*
! 	 * Read the arguments, one by one.
  	 */
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		PLpgSQL_expr *item;
! 		int		endtoken;
! 		int		argpos;
! 		int		tok1,
! 				tok2;
! 		int		arglocation;
! 
! 		/* Check if it's a named parameter: "param := value" */
! 		plpgsql_peek2(&tok1, &tok2, &arglocation, NULL);
! 		if (tok1 == IDENT && tok2 == COLON_EQUALS)
! 		{
! 			char   *argname;
! 
! 			/* Read the argument name, and find its position  */
! 			yylex();
! 			argname = yylval.str;
! 
! 			for (argpos = 0; argpos < row->nfields; argpos++)
! 			{
! 				if (strcmp(row->fieldnames[argpos], argname) == 0)
! 					break;
! 			}
! 			if (argpos == row->nfields)
! 				ereport(ERROR,
! 						(errcode(ERRCODE_SYNTAX_ERROR),
! 						 errmsg("cursor \"%s\" has no argument named \"%s\"",
! 								cursor->refname, argname),
! 						 parser_errposition(yylloc)));
! 
! 			/*
! 			 * Eat the ":=". We already peeked, so the error should never
! 			 * happen.
! 			 */
! 			tok2 = yylex();
! 			if (tok2 != COLON_EQUALS)
! 				yyerror("syntax error");
! 
! 			named = true;
! 		}
! 		else
! 			argpos = argc;
! 
! 		/*
! 		 * Read the value expression. To provide the user with meaningful
! 		 * parse error positions, we check the syntax immediately, instead of
! 		 * checking the final expression that may have the arguments
! 		 * reordered. Trailing whitespace must not be trimmed, because
! 		 * otherwise input of the form (param -- comment\n, param) would be
! 		 * translated into a form where the second parameter is commented
! 		 * out.
! 		 */
! 		item = read_sql_construct(',', ')', 0,
! 								  ",\" or \")",
! 								  sqlstart,
! 								  true, true,
! 								  false, /* do not trim */
! 								  NULL, &endtoken);
! 
! 		if (endtoken == ')' && !(argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("not enough arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (endtoken == ',' && (argc == row->nfields - 1))
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("too many arguments for cursor \"%s\"",
! 							cursor->refname),
! 					 parser_errposition(yylloc)));
! 
! 		if (argv[argpos] != NULL)
! 			ereport(ERROR,
! 					(errcode(ERRCODE_SYNTAX_ERROR),
! 					 errmsg("duplicate value for cursor \"%s\" parameter \"%s\"",
! 							cursor->refname, row->fieldnames[argpos]),
! 					 parser_errposition(arglocation)));
! 
! 		argv[argpos] = item->query + strlen(sqlstart);
! 	}
! 
! 	/* Make positional argument list */
! 	initStringInfo(&ds);
! 	appendStringInfoString(&ds, sqlstart);
! 	for (argc = 0; argc < row->nfields; argc++)
! 	{
! 		Assert(argv[argc] != NULL);
! 
! 		/*
! 		 * Because named notation allows permutated argument lists, include
! 		 * the parameter name for meaningful runtime errors.
! 		 */
! 		appendStringInfoString(&ds, argv[argc]);
! 		if (named)
! 			appendStringInfo(&ds, " AS %s",
! 							 quote_identifier(row->fieldnames[argc]));
! 		if (argc < row->nfields - 1)
! 			appendStringInfoString(&ds, ", ");
! 	}
! 	appendStringInfoChar(&ds, ';');
! 
! 	expr = palloc0(sizeof(PLpgSQL_expr));
! 	expr->dtype			= PLPGSQL_DTYPE_EXPR;
! 	expr->query			= pstrdup(ds.data);
! 	expr->plan			= NULL;
! 	expr->paramnos		= NULL;
! 	expr->ns            = plpgsql_ns_top();
! 	pfree(ds.data);
  
  	/* Next we'd better find the until token */
  	tok = yylex();
*** a/src/pl/plpgsql/src/pl_scanner.c
--- b/src/pl/plpgsql/src/pl_scanner.c
***************
*** 424,429 **** plpgsql_append_source_text(StringInfo buf,
--- 424,459 ----
  }
  
  /*
+  * Peek two tokens ahead in the input stream. The first token and its
+  * location the query are returned in *tok1_p and *tok1_loc, second token
+  * and its location in *tok2_p and *tok2_loc.
+  *
+  * NB: no variable or unreserved keyword lookup is performed here, they will
+  * be returned as IDENT. Reserved keywords are resolved as usual.
+  */
+ void
+ plpgsql_peek2(int *tok1_p, int *tok2_p, int *tok1_loc, int *tok2_loc)
+ {
+ 	int			tok1,
+ 				tok2;
+ 	TokenAuxData aux1,
+ 				aux2;
+ 
+ 	tok1 = internal_yylex(&aux1);
+ 	tok2 = internal_yylex(&aux2);
+ 
+ 	*tok1_p = tok1;
+ 	if (tok1_loc)
+ 		*tok1_loc = aux1.lloc;
+ 	*tok2_p = tok2;
+ 	if (tok2_loc)
+ 		*tok2_loc = aux2.lloc;
+ 
+ 	push_back_token(tok2, &aux2);
+ 	push_back_token(tok1, &aux1);
+ }
+ 
+ /*
   * plpgsql_scanner_errposition
   *		Report an error cursor position, if possible.
   *
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
***************
*** 962,967 **** extern int	plpgsql_yylex(void);
--- 962,969 ----
  extern void plpgsql_push_back_token(int token);
  extern void plpgsql_append_source_text(StringInfo buf,
  						   int startlocation, int endlocation);
+ extern void plpgsql_peek2(int *tok1_p, int *tok2_p, int *tok1_loc,
+ 			  int *tok2_loc);
  extern int	plpgsql_scanner_errposition(int location);
  extern void plpgsql_yyerror(const char *message);
  extern int	plpgsql_location_to_lineno(int location);
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
***************
*** 2292,2297 **** select refcursor_test2(20000, 20000) as "Should be false",
--- 2292,2426 ----
  (1 row)
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+  Should be false | Should be true 
+ -----------------+----------------
+  f               | t
+ (1 row)
+ 
+ -- mixing named and positional argument notations
+ create function namedparmcursor_test2(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+     nonsense record;
+ begin
+     open c1(param1 := $1, $2);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ select namedparmcursor_test2(20, 20);
+  namedparmcursor_test2 
+ -----------------------
+  t
+ (1 row)
+ 
+ -- mixing named and positional: param2 is given twice, once in named notation
+ -- and second time in positional notation. Should throw an error at parse time
+ create function namedparmcursor_test3() returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := 20, 21);
+ end
+ $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "param2"
+ LINE 5:     open c1(param2 := 20, 21);
+                                   ^
+ -- mixing named and positional: same as previous test, but param1 is duplicated
+ create function namedparmcursor_test4() returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(20, param1 := 21);
+ end
+ $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "param1"
+ LINE 5:     open c1(20, param1 := 21);
+                         ^
+ -- duplicate named parameter, should throw an error at parse time
+ create function namedparmcursor_test5() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end
+ $$ language plpgsql;
+ ERROR:  duplicate value for cursor "c1" parameter "p2"
+ LINE 6:   open c1 (p2 := 77, p2 := 42);
+                              ^
+ -- not enough parameters, should throw an error at parse time
+ create function namedparmcursor_test6() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77);
+ end
+ $$ language plpgsql;
+ ERROR:  not enough arguments for cursor "c1"
+ LINE 6:   open c1 (p2 := 77);
+                            ^
+ -- division by zero runtime error, the context given in the error message
+ -- should be sensible
+ create function namedparmcursor_test7() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select namedparmcursor_test7();
+ ERROR:  division by zero
+ CONTEXT:  SQL statement "SELECT 42/0 AS p1, 77 AS p2;"
+ PL/pgSQL function "namedparmcursor_test7" line 6 at OPEN
+ -- check that line comments work correctly within the argument list (there
+ -- is some special handling of this case in the code: the newline after the
+ -- comment must be preserved when the argument-evaluating query is
+ -- constructed, otherwise the comment effectively comments out the next
+ -- argument, too)
+ create function namedparmcursor_test8() returns int4 as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select count(*) from tenk1 where thousand = p1 and tenthous = p2;
+   n int4;
+ begin
+   open c1 (77 -- test
+   , 42);
+   fetch c1 into n;
+   return n;
+ end $$ language plpgsql;
+ select namedparmcursor_test8();
+  namedparmcursor_test8 
+ -----------------------
+                      0
+ (1 row)
+ 
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
***************
*** 1946,1951 **** select refcursor_test2(20000, 20000) as "Should be false",
--- 1946,2059 ----
         refcursor_test2(20, 20) as "Should be true";
  
  --
+ -- tests for cursors with named parameter arguments
+ --
+ create function namedparmcursor_test1(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param12 int) for select * from rc_test where a > param1 and b > param12;
+     nonsense record;
+ begin
+     open c1(param12 := $2, param1 := $1);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ 
+ select namedparmcursor_test1(20000, 20000) as "Should be false",
+        namedparmcursor_test1(20, 20) as "Should be true";
+ 
+ -- mixing named and positional argument notations
+ create function namedparmcursor_test2(int, int) returns boolean as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+     nonsense record;
+ begin
+     open c1(param1 := $1, $2);
+     fetch c1 into nonsense;
+     close c1;
+     if found then
+         return true;
+     else
+         return false;
+     end if;
+ end
+ $$ language plpgsql;
+ select namedparmcursor_test2(20, 20);
+ 
+ -- mixing named and positional: param2 is given twice, once in named notation
+ -- and second time in positional notation. Should throw an error at parse time
+ create function namedparmcursor_test3() returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(param2 := 20, 21);
+ end
+ $$ language plpgsql;
+ 
+ -- mixing named and positional: same as previous test, but param1 is duplicated
+ create function namedparmcursor_test4() returns void as $$
+ declare
+     c1 cursor (param1 int, param2 int) for select * from rc_test where a > param1 and b > param2;
+ begin
+     open c1(20, param1 := 21);
+ end
+ $$ language plpgsql;
+ 
+ -- duplicate named parameter, should throw an error at parse time
+ create function namedparmcursor_test5() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p2 := 42);
+ end
+ $$ language plpgsql;
+ 
+ -- not enough parameters, should throw an error at parse time
+ create function namedparmcursor_test6() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77);
+ end
+ $$ language plpgsql;
+ 
+ -- division by zero runtime error, the context given in the error message
+ -- should be sensible
+ create function namedparmcursor_test7() returns void as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select * from tenk1 where thousand = p1 and tenthous = p2;
+ begin
+   open c1 (p2 := 77, p1 := 42/0);
+ end $$ language plpgsql;
+ select namedparmcursor_test7();
+ 
+ -- check that line comments work correctly within the argument list (there
+ -- is some special handling of this case in the code: the newline after the
+ -- comment must be preserved when the argument-evaluating query is
+ -- constructed, otherwise the comment effectively comments out the next
+ -- argument, too)
+ create function namedparmcursor_test8() returns int4 as $$
+ declare
+   c1 cursor (p1 int, p2 int) for
+     select count(*) from tenk1 where thousand = p1 and tenthous = p2;
+   n int4;
+ begin
+   open c1 (77 -- test
+   , 42);
+   fetch c1 into n;
+   return n;
+ end $$ language plpgsql;
+ select namedparmcursor_test8();
+ 
+ --
  -- tests for "raise" processing
  --
  create function raise_test1(int) returns int as $$
#11Tom Lane
tgl@sss.pgh.pa.us
In reply to: Heikki Linnakangas (#10)

Heikki Linnakangas <heikki.linnakangas@enterprisedb.com> writes:

Attached is a patch with those changes. I also I removed a few of the
syntax error regression tests, that seemed excessive, plus some general
naming and comment fiddling. I'll apply this tomorrow, if it still looks
good to me after sleeping on it.

The code looks reasonably clean now, although I noted one comment
thinko:

+ * bool: trim trailing whitespace

ITYM

+ * trim: trim trailing whitespace

However, I'm still concerned about whether this approach gives
reasonable error messages in cases where the error would be
detected during parse analysis of the rearranged statement.
The regression test examples don't cover such cases, and I'm
too busy right now to apply the patch and check for myself.
What happens for example if a named parameter's value contains
a misspelled variable reference, or a type conflict?

regards, tom lane

#12Yeb Havinga
yebhavinga@gmail.com
In reply to: Tom Lane (#11)

On 2011-12-13 18:34, Tom Lane wrote:

Heikki Linnakangas<heikki.linnakangas@enterprisedb.com> writes:

Attached is a patch with those changes. I also I removed a few of the
syntax error regression tests, that seemed excessive, plus some general
naming and comment fiddling. I'll apply this tomorrow, if it still looks
good to me after sleeping on it.

However, I'm still concerned about whether this approach gives
reasonable error messages in cases where the error would be
detected during parse analysis of the rearranged statement.
The regression test examples don't cover such cases, and I'm
too busy right now to apply the patch and check for myself.
What happens for example if a named parameter's value contains
a misspelled variable reference, or a type conflict?

I tested this and seems to be ok:

regression=# select namedparmcursor_test1(20000, 20000) as "Should be
false",
namedparmcursor_test1(20, 20) as "Should be true";
ERROR: column "yy" does not exist
LINE 1: SELECT x AS param1, yy AS param12;

regression=# select namedparmcursor_test1(20000, 20000) as "Should be
false",
namedparmcursor_test1(20, 20) as "Should be true";
ERROR: invalid input syntax for integer: "2011-11-29 19:26:10.029084"
CONTEXT: PL/pgSQL function "namedparmcursor_test1" line 8 at OPEN

regards,
Yeb Havinga

last error was created with

create or replace function namedparmcursor_test1(int, int) returns
boolean as $$
declare
c1 cursor (param1 int, param12 int) for select * from rc_test where
a > param1 and b > param12;
y int := 10;
x timestamp := now();
nonsense record;
begin
open c1(param12 := $1, param1 := x);
end
$$ language plpgsql;

#13Heikki Linnakangas
heikki.linnakangas@enterprisedb.com
In reply to: Yeb Havinga (#12)

On 14.12.2011 12:31, Yeb Havinga wrote:

On 2011-12-13 18:34, Tom Lane wrote:

Heikki Linnakangas<heikki.linnakangas@enterprisedb.com> writes:

Attached is a patch with those changes. I also I removed a few of the
syntax error regression tests, that seemed excessive, plus some general
naming and comment fiddling. I'll apply this tomorrow, if it still looks
good to me after sleeping on it.

However, I'm still concerned about whether this approach gives
reasonable error messages in cases where the error would be
detected during parse analysis of the rearranged statement.
The regression test examples don't cover such cases, and I'm
too busy right now to apply the patch and check for myself.
What happens for example if a named parameter's value contains
a misspelled variable reference, or a type conflict?

I tested this and seems to be ok:

regression=# select namedparmcursor_test1(20000, 20000) as "Should be
false",
namedparmcursor_test1(20, 20) as "Should be true";
ERROR: column "yy" does not exist
LINE 1: SELECT x AS param1, yy AS param12;

For the record, the caret pointing to the position is there, too. As in:

regression=# do $$
declare
c1 cursor (param1 int, param2 int) for select 123;
begin
open c1(param2 := xxx, param1 := 123);
end;
$$;
ERROR: column "xxx" does not exist
LINE 1: SELECT 123 AS param1, xxx AS param2;
^
QUERY: SELECT 123 AS param1, xxx AS param2;
CONTEXT: PL/pgSQL function "inline_code_block" line 5 at OPEN

I think that's good enough. It would be even better if we could print
the original OPEN statement as the context, as in:

ERROR: column "xxx" does not exist
LINE 4: OPEN c1(param2 := xxx, param1 := 123);
^

but it didn't look quite like that before the patch either, and isn't
specific to this patch but more of a general usability issue in PL/pgSQL.

Committed.

--
Heikki Linnakangas
EnterpriseDB http://www.enterprisedb.com