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 **** ! Opening a Bound Cursor ! OPEN bound_cursorvar ( argument_values ) ; --- 2823,2833 ---- ! Opening a Bound Cursor ! OPEN bound_cursorvar ( argname := argument_value , ... ) ; *************** OPEN bound_cursorvar + Cursors may be opened using either positional + or named notation. In contrast with calling + functions, described in , 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 := to + separate it from the argument expression. + + + Examples (these use the cursor declaration examples above): OPEN curs2; OPEN curs3(42); + OPEN curs3(key := 42); *************** COMMIT; *** 3169,3186 **** <<label>> ! FOR recordvar IN bound_cursorvar ( argument_values ) LOOP statements END LOOP label ; The cursor variable must have been bound to some query when it was ! declared, and it cannot be open already. The ! 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 OPEN. The variable recordvar is automatically defined as type record and exists only inside the loop (any existing definition of the variable name is ignored within the loop). --- 3180,3203 ---- <<label>> ! FOR recordvar IN bound_cursorvar ( argname := argument_value , ... ) LOOP statements END LOOP label ; The cursor variable must have been bound to some query when it was ! declared, and it cannot be open already. The 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 ! positional or named ! 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 ! OPEN described in . ! ! ! The variable recordvar is automatically defined as 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 $$