Greetings fellow elephants,

I would humbly like to submit for your consideration my proposal for alleviating pain caused by one of the most annoying footguns in PL/PgSQL: the behaviour of SELECT .. INTO when the query returns more than one row. Some of you might know that no exception is raised in this case (as opposed to INSERT/UPDATE/DELETE .. INTO, all of them yielding TOO_MANY_ROWS), which can hide subtle bugs in queries if during testing the query always returns only one row or the "correct" one happens to be picked up every time. Additionally, the row_count() after execution is always going to be either 0 or 1, so even if you want to explicitly guard against potentially broken queries, you can't do so!

So I added the following compile-time option:


set plpgsql.consistent_into to true;

create or replace function footest() returns void as $$
declare
x int;
begin
  -- too many rows
  select 1 from foo into x;
end$$ language plpgsql;

select footest();
ERROR:  query returned more than one row

It defaults to false to preserve full backwards compatibility. Also turning it on makes the executor try and find two rows, so it might have an effect on performance as well. The patch, as currently written, also changes the behaviour of EXECUTE .. INTO, but I don't feel strongly about whether that should be affected as well or not.


Regards,
Marko Tiikkaja
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
***************
*** 1043,1050 **** DELETE ... RETURNING <replaceable>expressions</replaceable> 
INTO <optional>STRIC
       clause, then <replaceable>target</replaceable> will be set to the first
       row returned by the query, or to nulls if the query returned no rows.
       (Note that <quote>the first row</> is not
!      well-defined unless you've used <literal>ORDER BY</>.)  Any result rows
!      after the first row are discarded.
       You can check the special <literal>FOUND</literal> variable (see
       <xref linkend="plpgsql-statements-diagnostics">) to
       determine whether a row was returned:
--- 1043,1053 ----
       clause, then <replaceable>target</replaceable> will be set to the first
       row returned by the query, or to nulls if the query returned no rows.
       (Note that <quote>the first row</> is not
!      well-defined unless you've used <literal>ORDER BY</>.)  By default,
!      any result rows after the first row are discarded.  However, if the
!      <literal>consistent_into</> compile option has been enabled and the
!      query returns more than one row, the run-time error
!      <literal>TOO_MANY_ROWS</> will be reported.
       You can check the special <literal>FOUND</literal> variable (see
       <xref linkend="plpgsql-statements-diagnostics">) to
       determine whether a row was returned:
***************
*** 1172,1178 **** EXECUTE <replaceable 
class="command">command-string</replaceable> <optional> INT
       record variable is used, it will configure itself to match the
       result structure automatically). If multiple rows are returned,
       only the first will be assigned to the <literal>INTO</literal>
!      variable. If no rows are returned, NULL is assigned to the
       <literal>INTO</literal> variable(s). If no <literal>INTO</literal>
       clause is specified, the query results are discarded.
      </para>
--- 1175,1183 ----
       record variable is used, it will configure itself to match the
       result structure automatically). If multiple rows are returned,
       only the first will be assigned to the <literal>INTO</literal>
!      variable, unless <literal>consistent_into</literal> has been
!      enabled, in which case <literal>TOO_MANY_ROWS</literal> is raised.
!      If no rows are returned, NULL is assigned to the
       <literal>INTO</literal> variable(s). If no <literal>INTO</literal>
       clause is specified, the query results are discarded.
      </para>
*** a/src/pl/plpgsql/src/pl_comp.c
--- b/src/pl/plpgsql/src/pl_comp.c
***************
*** 352,357 **** do_compile(FunctionCallInfo fcinfo,
--- 352,358 ----
        function->out_param_varno = -1;         /* set up for no OUT param */
        function->resolve_option = plpgsql_variable_conflict;
        function->print_strict_params = plpgsql_print_strict_params;
+       function->consistent_into = plpgsql_consistent_into;
  
        if (is_dml_trigger)
                function->fn_is_trigger = PLPGSQL_DML_TRIGGER;
***************
*** 849,854 **** plpgsql_compile_inline(char *proc_source)
--- 850,856 ----
        function->out_param_varno = -1;         /* set up for no OUT param */
        function->resolve_option = plpgsql_variable_conflict;
        function->print_strict_params = plpgsql_print_strict_params;
+       function->consistent_into = plpgsql_consistent_into;
  
        plpgsql_ns_init();
        plpgsql_ns_push(func_name);
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
***************
*** 3308,3316 **** exec_stmt_execsql(PLpgSQL_execstate *estate,
  
        /*
         * If we have INTO, then we only need one row back ... but if we have 
INTO
!        * STRICT, ask for two rows, so that we can verify the statement returns
!        * only one.  INSERT/UPDATE/DELETE are always treated strictly. Without
!        * INTO, just run the statement to completion (tcount = 0).
         *
         * We could just ask for two rows always when using INTO, but there are
         * some cases where demanding the extra row costs significant time, eg 
by
--- 3308,3317 ----
  
        /*
         * If we have INTO, then we only need one row back ... but if we have 
INTO
!        * STRICT or consistent_into is enabled, ask for two rows, so that we 
can
!        * verify the statement returns only one.  INSERT/UPDATE/DELETE are 
always
!        * treated strictly. Without INTO, just run the statement to completion
!        * (tcount = 0).
         *
         * We could just ask for two rows always when using INTO, but there are
         * some cases where demanding the extra row costs significant time, eg 
by
***************
*** 3319,3325 **** exec_stmt_execsql(PLpgSQL_execstate *estate,
         */
        if (stmt->into)
        {
!               if (stmt->strict || stmt->mod_stmt)
                        tcount = 2;
                else
                        tcount = 1;
--- 3320,3326 ----
         */
        if (stmt->into)
        {
!               if (stmt->strict || stmt->mod_stmt || 
estate->func->consistent_into)
                        tcount = 2;
                else
                        tcount = 1;
***************
*** 3439,3445 **** exec_stmt_execsql(PLpgSQL_execstate *estate,
                }
                else
                {
!                       if (n > 1 && (stmt->strict || stmt->mod_stmt))
                        {
                                char *errdetail;
  
--- 3440,3446 ----
                }
                else
                {
!                       if (n > 1 && (stmt->strict || stmt->mod_stmt || 
estate->func->consistent_into))
                        {
                                char *errdetail;
  
***************
*** 3606,3614 **** exec_stmt_dynexecute(PLpgSQL_execstate *estate,
                        elog(ERROR, "unsupported target");
  
                /*
!                * If SELECT ... INTO specified STRICT, and the query didn't 
find
!                * exactly one row, throw an error.  If STRICT was not 
specified, then
!                * allow the query to find any number of rows.
                 */
                if (n == 0)
                {
--- 3607,3615 ----
                        elog(ERROR, "unsupported target");
  
                /*
!                * If SELECT ... INTO specified STRICT or consistent_info is 
enabled,
!                * and the query didn't find exactly one row, throw an error.
!                * Otherwise we allow the query to find any number of rows.
                 */
                if (n == 0)
                {
***************
*** 3631,3637 **** exec_stmt_dynexecute(PLpgSQL_execstate *estate,
                }
                else
                {
!                       if (n > 1 && stmt->strict)
                        {
                                char *errdetail;
  
--- 3632,3638 ----
                }
                else
                {
!                       if (n > 1 && (stmt->strict || 
estate->func->consistent_into))
                        {
                                char *errdetail;
  
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
***************
*** 185,191 **** static List                    *read_raise_options(void);
  %type <forvariable>   for_variable
  %type <stmt>  for_control
  
! %type <str>           any_identifier opt_block_label opt_label option_value
  
  %type <list>  proc_sect proc_stmts stmt_elsifs stmt_else
  %type <loop_body>     loop_body
--- 185,192 ----
  %type <forvariable>   for_variable
  %type <stmt>  for_control
  
! %type <str>           any_identifier opt_block_label opt_label
! %type <boolean> option_value
  
  %type <list>  proc_sect proc_stmts stmt_elsifs stmt_else
  %type <loop_body>     loop_body
***************
*** 253,258 **** static List                    *read_raise_options(void);
--- 254,260 ----
  %token <keyword>      K_COLLATE
  %token <keyword>      K_COLUMN
  %token <keyword>      K_COLUMN_NAME
+ %token <keyword>      K_CONSISTENT_INTO
  %token <keyword>      K_CONSTANT
  %token <keyword>      K_CONSTRAINT
  %token <keyword>      K_CONSTRAINT_NAME
***************
*** 355,368 **** comp_option            : '#' K_OPTION K_DUMP
                                        {
                                                plpgsql_DumpExecTree = true;
                                        }
                                | '#' K_PRINT_STRICT_PARAMS option_value
                                        {
!                                               if (strcmp($3, "on") == 0)
!                                                       
plpgsql_curr_compile->print_strict_params = true;
!                                               else if (strcmp($3, "off") == 0)
!                                                       
plpgsql_curr_compile->print_strict_params = false;
!                                               else
!                                                       elog(ERROR, 
"unrecognized print_strict_params option %s", $3);
                                        }
                                | '#' K_VARIABLE_CONFLICT K_ERROR
                                        {
--- 357,369 ----
                                        {
                                                plpgsql_DumpExecTree = true;
                                        }
+                               | '#' K_CONSISTENT_INTO option_value
+                                       {
+                                               
plpgsql_curr_compile->consistent_into = $3;
+                                       }
                                | '#' K_PRINT_STRICT_PARAMS option_value
                                        {
!                                               
plpgsql_curr_compile->print_strict_params = $3;
                                        }
                                | '#' K_VARIABLE_CONFLICT K_ERROR
                                        {
***************
*** 380,390 **** comp_option            : '#' K_OPTION K_DUMP
  
  option_value : T_WORD
                                {
!                                       $$ = $1.ident;
                                }
                         | unreserved_keyword
                                {
!                                       $$ = pstrdup($1);
                                }
  
  opt_semi              :
--- 381,401 ----
  
  option_value : T_WORD
                                {
!                                       if (strcmp($1.ident, "on") == 0)
!                                               $$ = true;
!                                       else if (strcmp($1.ident, "off") == 0)
!                                               $$ = false;
!                                       else
!                                               elog(ERROR, "unrecognized 
option_value %s", $1.ident);
                                }
                         | unreserved_keyword
                                {
!                                       if (strcmp($1, "on") == 0)
!                                               $$ = true;
!                                       else if (strcmp($1, "off") == 0)
!                                               $$ = false;
!                                       else
!                                               elog(ERROR, "unrecognized 
option_value %s", $1);
                                }
  
  opt_semi              :
*** a/src/pl/plpgsql/src/pl_handler.c
--- b/src/pl/plpgsql/src/pl_handler.c
***************
*** 38,43 **** static const struct config_enum_entry 
variable_conflict_options[] = {
--- 38,44 ----
  int                   plpgsql_variable_conflict = PLPGSQL_RESOLVE_ERROR;
  
  bool          plpgsql_print_strict_params = false;
+ bool          plpgsql_consistent_into = false;
  
  /* Hook for plugins */
  PLpgSQL_plugin **plugin_ptr = NULL;
***************
*** 75,80 **** _PG_init(void)
--- 76,89 ----
                                                         false,
                                                         PGC_USERSET, 0,
                                                         NULL, NULL, NULL);
+       DefineCustomBoolVariable("plpgsql.consistent_into",
+                                                        gettext_noop("Always 
raise an exception if a statement with INTO returns more than one row."),
+                                                        NULL,
+                                                        
&plpgsql_consistent_into,
+                                                        false,
+                                                        PGC_USERSET, 0,
+                                                        NULL, NULL, NULL);
+ 
  
        EmitWarningsOnPlaceholders("plpgsql");
  
*** a/src/pl/plpgsql/src/pl_scanner.c
--- b/src/pl/plpgsql/src/pl_scanner.c
***************
*** 111,116 **** static const ScanKeyword unreserved_keywords[] = {
--- 111,117 ----
        PG_KEYWORD("backward", K_BACKWARD, UNRESERVED_KEYWORD)
        PG_KEYWORD("column", K_COLUMN, UNRESERVED_KEYWORD)
        PG_KEYWORD("column_name", K_COLUMN_NAME, UNRESERVED_KEYWORD)
+       PG_KEYWORD("consistent_into", K_CONSISTENT_INTO, UNRESERVED_KEYWORD)
        PG_KEYWORD("constant", K_CONSTANT, UNRESERVED_KEYWORD)
        PG_KEYWORD("constraint", K_CONSTRAINT, UNRESERVED_KEYWORD)
        PG_KEYWORD("constraint_name", K_CONSTRAINT_NAME, UNRESERVED_KEYWORD)
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
***************
*** 738,743 **** typedef struct PLpgSQL_function
--- 738,744 ----
        PLpgSQL_resolve_option resolve_option;
  
        bool            print_strict_params;
+       bool            consistent_into;
  
        int                     ndatums;
        PLpgSQL_datum **datums;
***************
*** 880,885 **** extern IdentifierLookup plpgsql_IdentifierLookup;
--- 881,887 ----
  extern int    plpgsql_variable_conflict;
  
  extern bool plpgsql_print_strict_params;
+ extern bool plpgsql_consistent_into;
  
  extern bool plpgsql_check_syntax;
  extern bool plpgsql_DumpExecTree;
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
***************
*** 3208,3213 **** select footest();
--- 3208,3236 ----
  ERROR:  query returned more than one row
  DETAIL:  parameters: p1 = '2', p3 = 'foo'
  CONTEXT:  PL/pgSQL function footest() line 10 at SQL statement
+ -- test consistent INTO
+ set plpgsql.consistent_into to true;
+ create or replace function footest() returns void as $$
+ declare
+ x int;
+ begin
+   -- too many rows
+   select 1 from foo into x;
+ end$$ language plpgsql;
+ select footest();
+ ERROR:  query returned more than one row
+ CONTEXT:  PL/pgSQL function footest() line 6 at SQL statement
+ create or replace function footest() returns void as $$
+ declare
+ x int;
+ begin
+   -- too many rows
+   execute $q$select 1 from foo$q$ into x;
+ end$$ language plpgsql;
+ select footest();
+ ERROR:  query returned more than one row
+ CONTEXT:  PL/pgSQL function footest() line 6 at EXECUTE statement
+ reset plpgsql.consistent_into;
  -- test scrollable cursor support
  create function sc_test() returns setof integer as $$
  declare
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
***************
*** 2689,2694 **** end$$ language plpgsql;
--- 2689,2720 ----
  
  select footest();
  
+ -- test consistent INTO
+ 
+ set plpgsql.consistent_into to true;
+ 
+ create or replace function footest() returns void as $$
+ declare
+ x int;
+ begin
+   -- too many rows
+   select 1 from foo into x;
+ end$$ language plpgsql;
+ 
+ select footest();
+ 
+ create or replace function footest() returns void as $$
+ declare
+ x int;
+ begin
+   -- too many rows
+   execute $q$select 1 from foo$q$ into x;
+ end$$ language plpgsql;
+ 
+ select footest();
+ 
+ reset plpgsql.consistent_into;
+ 
  -- test scrollable cursor support
  
  create function sc_test() returns setof integer as $$
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to