plpgsql.extra_warnings='num_into_expressions'

Started by Marko Tiikkajaover 11 years ago8 messages
#1Marko Tiikkaja
marko@joh.to
1 attachment(s)

Hi again,

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

.marko

Attachments:

plpgsql_num_into_expressions_v0.patchtext/plain; charset=UTF-8; name=plpgsql_num_into_expressions_v0.patch; x-mac-creator=0; x-mac-type=0Download
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
***************
*** 4730,4736 **** a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
    <varname>plpgsql.extra_errors</> for errors. Both can be set either to
    a comma-separated list of checks, <literal>"none"</> or <literal>"all"</>.
    The default is <literal>"none"</>. Currently the list of available checks
!   includes only one:
    <variablelist>
     <varlistentry>
      <term><varname>shadowed_variables</varname></term>
--- 4730,4736 ----
    <varname>plpgsql.extra_errors</> for errors. Both can be set either to
    a comma-separated list of checks, <literal>"none"</> or <literal>"all"</>.
    The default is <literal>"none"</>. Currently the list of available checks
!   is as follows:
    <variablelist>
     <varlistentry>
      <term><varname>shadowed_variables</varname></term>
***************
*** 4740,4745 **** a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
--- 4740,4755 ----
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><varname>num_into_expressions</varname></term>
+     <listitem>
+      <para>
+       When possible, checks that the number of expressions specified in the
+       SELECT or RETURNING list of a query matches the number expected by the
+       INTO target.  This check is done on a "best effort" basis.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
  
    The following example shows the effect of <varname>plpgsql.extra_warnings</>
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
***************
*** 22,27 ****
--- 22,28 ----
  #include "parser/scanner.h"
  #include "parser/scansup.h"
  #include "utils/builtins.h"
+ #include "nodes/nodefuncs.h"
  
  
  /* Location tracking support --- simpler than bison's default */
***************
*** 97,103 **** static	PLpgSQL_row		*make_scalar_list1(char *initial_name,
  										   PLpgSQL_datum *initial_datum,
  										   int lineno, int location);
  static	void			 check_sql_expr(const char *stmt, int location,
! 										int leaderlen);
  static	void			 plpgsql_sql_error_callback(void *arg);
  static	PLpgSQL_type	*parse_datatype(const char *string, int location);
  static	void			 check_labels(const char *start_label,
--- 98,104 ----
  										   PLpgSQL_datum *initial_datum,
  										   int lineno, int location);
  static	void			 check_sql_expr(const char *stmt, int location,
! 										int leaderlen, PLpgSQL_row *check_row);
  static	void			 plpgsql_sql_error_callback(void *arg);
  static	PLpgSQL_type	*parse_datatype(const char *string, int location);
  static	void			 check_labels(const char *start_label,
***************
*** 106,111 **** static	void			 check_labels(const char *start_label,
--- 107,115 ----
  static	PLpgSQL_expr	*read_cursor_args(PLpgSQL_var *cursor,
  										  int until, const char *expected);
  static	List			*read_raise_options(void);
+ static	bool			find_a_star_walker(Node *node, void *context);
+ static	int				tlist_result_column_count(Node *stmt);
+ 
  
  %}
  
***************
*** 1438,1444 **** for_control		: for_variable K_IN
  								PLpgSQL_stmt_fori	*new;
  
  								/* Check first expression is well-formed */
! 								check_sql_expr(expr1->query, expr1loc, 7);
  
  								/* Read and check the second one */
  								expr2 = read_sql_expression2(K_LOOP, K_BY,
--- 1442,1448 ----
  								PLpgSQL_stmt_fori	*new;
  
  								/* Check first expression is well-formed */
! 								check_sql_expr(expr1->query, expr1loc, 7, NULL);
  
  								/* Read and check the second one */
  								expr2 = read_sql_expression2(K_LOOP, K_BY,
***************
*** 1500,1506 **** for_control		: for_variable K_IN
  								pfree(expr1->query);
  								expr1->query = tmp_query;
  
! 								check_sql_expr(expr1->query, expr1loc, 0);
  
  								new = palloc0(sizeof(PLpgSQL_stmt_fors));
  								new->cmd_type = PLPGSQL_STMT_FORS;
--- 1504,1510 ----
  								pfree(expr1->query);
  								expr1->query = tmp_query;
  
! 								check_sql_expr(expr1->query, expr1loc, 0, NULL);
  
  								new = palloc0(sizeof(PLpgSQL_stmt_fors));
  								new->cmd_type = PLPGSQL_STMT_FORS;
***************
*** 2592,2598 **** read_sql_construct(int until,
  	pfree(ds.data);
  
  	if (valid_sql)
! 		check_sql_expr(expr->query, startlocation, strlen(sqlstart));
  
  	return expr;
  }
--- 2596,2602 ----
  	pfree(ds.data);
  
  	if (valid_sql)
! 		check_sql_expr(expr->query, startlocation, strlen(sqlstart), NULL);
  
  	return expr;
  }
***************
*** 2815,2821 **** make_execsql_stmt(int firsttoken, int location)
  	expr->ns			= plpgsql_ns_top();
  	pfree(ds.data);
  
! 	check_sql_expr(expr->query, location, 0);
  
  	execsql = palloc(sizeof(PLpgSQL_stmt_execsql));
  	execsql->cmd_type = PLPGSQL_STMT_EXECSQL;
--- 2819,2825 ----
  	expr->ns			= plpgsql_ns_top();
  	pfree(ds.data);
  
! 	check_sql_expr(expr->query, location, 0, row);
  
  	execsql = palloc(sizeof(PLpgSQL_stmt_execsql));
  	execsql->cmd_type = PLPGSQL_STMT_EXECSQL;
***************
*** 3409,3421 **** make_scalar_list1(char *initial_name,
   * (typically "SELECT ") prefixed to the source text.  We use this assumption
   * to transpose any error cursor position back to the function source text.
   * If no error cursor is provided, we'll just point at "location".
   */
  static void
! check_sql_expr(const char *stmt, int location, int leaderlen)
  {
  	sql_error_callback_arg cbarg;
  	ErrorContextCallback  syntax_errcontext;
  	MemoryContext oldCxt;
  
  	if (!plpgsql_check_syntax)
  		return;
--- 3413,3430 ----
   * (typically "SELECT ") prefixed to the source text.  We use this assumption
   * to transpose any error cursor position back to the function source text.
   * If no error cursor is provided, we'll just point at "location".
+  *
+  * If check_row is not NULL and PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS is enabled,
+  * the statement's result column count is compared against it when the exact
+  * number of columns is known and an error or a warning is reported.
   */
  static void
! check_sql_expr(const char *stmt, int location, int leaderlen, PLpgSQL_row *check_row)
  {
  	sql_error_callback_arg cbarg;
  	ErrorContextCallback  syntax_errcontext;
  	MemoryContext oldCxt;
+ 	List *raw_parsetree_list;
  
  	if (!plpgsql_check_syntax)
  		return;
***************
*** 3429,3441 **** check_sql_expr(const char *stmt, int location, int leaderlen)
  	error_context_stack = &syntax_errcontext;
  
  	oldCxt = MemoryContextSwitchTo(compile_tmp_cxt);
! 	(void) raw_parser(stmt);
  	MemoryContextSwitchTo(oldCxt);
  
  	/* Restore former ereport callback */
  	error_context_stack = syntax_errcontext.previous;
  }
  
  static void
  plpgsql_sql_error_callback(void *arg)
  {
--- 3438,3527 ----
  	error_context_stack = &syntax_errcontext;
  
  	oldCxt = MemoryContextSwitchTo(compile_tmp_cxt);
! 	raw_parsetree_list = raw_parser(stmt);
! 
! 	if (check_row != NULL &&
! 		(plpgsql_curr_compile->extra_warnings & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS ||
! 		 plpgsql_curr_compile->extra_errors & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS))
! 	{
! 		Node *raw_parse_tree;
! 		int ncols;
! 		int fnum;
! 		int expected_ncols = 0;
! 
! 		for (fnum = 0; fnum < check_row->nfields; fnum++)
! 		{
! 			if (check_row->varnos[fnum] < 0)
! 				continue;
! 			expected_ncols++;
! 		}
! 
! 		raw_parse_tree = linitial(raw_parsetree_list);
! 		ncols = tlist_result_column_count(raw_parse_tree);
! 		if (ncols >= 0 && ncols != expected_ncols)
! 			ereport(plpgsql_curr_compile->extra_errors & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS ? ERROR : WARNING,
! 				(errcode(ERRCODE_SYNTAX_ERROR),
! 				errmsg("the number of result expressions does not match that expected by the INTO target"),
! 				errdetail("INTO target expects %d expressions, but the query returns %d", expected_ncols, ncols)));
! 	}
  	MemoryContextSwitchTo(oldCxt);
  
  	/* Restore former ereport callback */
  	error_context_stack = syntax_errcontext.previous;
  }
  
+ /*
+  * Expression tree walker for tlist_result_column_count.  Returns true if the
+  * expression tree contains an A_Star node, false otherwise.
+  */
+ static bool
+ find_a_star_walker(Node *node, void *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, A_Star))
+ 		return true;
+ 	if (IsA(node, ColumnRef))
+ 	{
+ 		ColumnRef *ref = (ColumnRef *) node;
+ 		/* A_Star can only be the last element */
+ 		if (IsA(llast(ref->fields), A_Star))
+ 			return true;
+ 	}
+ 	return raw_expression_tree_walker((Node *) node,
+ 									  find_a_star_walker,
+ 									  context);
+ }
+ 
+ /*
+  * Find the number of columns in a raw statement's targetList (if SELECT) or
+  * returningList (if INSERT, UPDATE or DELETE).  Returns -1 if the number of
+  * columns could not be determined because of an A_Star.
+  */
+ static int
+ tlist_result_column_count(Node *stmt)
+ {
+ 	List *tlist;
+ 
+ 	if (IsA(stmt, SelectStmt))
+ 		tlist = ((SelectStmt *) stmt)->targetList;
+ 	else if (IsA(stmt, InsertStmt))
+ 		tlist = ((InsertStmt *) stmt)->returningList;
+ 	else if (IsA(stmt, UpdateStmt))
+ 		tlist = ((UpdateStmt *) stmt)->returningList;
+ 	else if (IsA(stmt, DeleteStmt))
+ 		tlist = ((DeleteStmt *) stmt)->returningList;
+ 	else
+ 		elog(ERROR, "unknown nodeTag %d", nodeTag(stmt));
+ 
+ 	if (tlist == NIL)
+ 		return 0;
+ 
+ 	if (raw_expression_tree_walker((Node *) tlist, find_a_star_walker, NULL))
+ 		return -1;
+ 	return list_length(tlist);
+ }
+ 
  static void
  plpgsql_sql_error_callback(void *arg)
  {
*** a/src/pl/plpgsql/src/pl_handler.c
--- b/src/pl/plpgsql/src/pl_handler.c
***************
*** 87,92 **** plpgsql_extra_checks_check_hook(char **newvalue, void **extra, GucSource source)
--- 87,94 ----
  
  			if (pg_strcasecmp(tok, "shadowed_variables") == 0)
  				extrachecks |= PLPGSQL_XCHECK_SHADOWVAR;
+ 			else if (pg_strcasecmp(tok, "num_into_expressions") == 0)
+ 				extrachecks |= PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS;
  			else if (pg_strcasecmp(tok, "all") == 0 || pg_strcasecmp(tok, "none") == 0)
  			{
  				GUC_check_errdetail("Key word \"%s\" cannot be combined with other key words.", tok);
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
***************
*** 886,894 **** extern int	plpgsql_variable_conflict;
  extern bool plpgsql_print_strict_params;
  
  /* extra compile-time checks */
! #define PLPGSQL_XCHECK_NONE			0
! #define PLPGSQL_XCHECK_SHADOWVAR	1
! #define PLPGSQL_XCHECK_ALL			((int) ~0)
  
  extern int	plpgsql_extra_warnings;
  extern int	plpgsql_extra_errors;
--- 886,895 ----
  extern bool plpgsql_print_strict_params;
  
  /* extra compile-time checks */
! #define PLPGSQL_XCHECK_NONE						0
! #define PLPGSQL_XCHECK_SHADOWVAR				1
! #define PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS		2
! #define PLPGSQL_XCHECK_ALL						((int) ~0)
  
  extern int	plpgsql_extra_warnings;
  extern int	plpgsql_extra_errors;
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
***************
*** 5340,5342 **** NOTICE:  outer_func() done
--- 5340,5388 ----
  drop function outer_outer_func(int);
  drop function outer_func(int);
  drop function inner_func(int);
+ create temporary table somecols(a int, b int);
+ set plpgsql.extra_warnings to 'num_into_expressions';
+ -- INTO column counts
+ create or replace function into_counts()
+ returns void as $$
+ declare
+   myresult int;
+   myrec record;
+   mycols somecols;
+ begin
+   -- no warning
+   select 1 into myresult;
+   -- we don't know the column count without parse analysis, no warning
+   select * into myresult from somecols;
+   -- records are OK
+   select 1, 2 into myrec;
+   -- warning
+   select a, b into myresult from somecols;
+   -- warning
+   select into myresult from somecols;
+   -- no warning
+   select 1, 2 into mycols;
+   -- warning
+   select 1 into mycols;
+   -- warning
+   select 1, 2, 3 into myresult;
+ end;
+ $$ language plpgsql;
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 15:   select a, b into myresult from somecols;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 2
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 17:   select into myresult from somecols;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 0
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 21:   select 1 into mycols;
+            ^
+ DETAIL:  INTO target expects 2 expressions, but the query returns 1
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 23:   select 1, 2, 3 into myresult;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 3
+ drop table somecols;
+ reset plpgsql.extra_warnings;
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
***************
*** 4193,4195 **** select outer_outer_func(20);
--- 4193,4230 ----
  drop function outer_outer_func(int);
  drop function outer_func(int);
  drop function inner_func(int);
+ 
+ create temporary table somecols(a int, b int);
+ 
+ set plpgsql.extra_warnings to 'num_into_expressions';
+ 
+ -- INTO column counts
+ create or replace function into_counts()
+ returns void as $$
+ declare
+   myresult int;
+   myrec record;
+   mycols somecols;
+ begin
+   -- no warning
+   select 1 into myresult;
+   -- we don't know the column count without parse analysis, no warning
+   select * into myresult from somecols;
+   -- records are OK
+   select 1, 2 into myrec;
+   -- warning
+   select a, b into myresult from somecols;
+   -- warning
+   select into myresult from somecols;
+   -- no warning
+   select 1, 2 into mycols;
+   -- warning
+   select 1 into mycols;
+   -- warning
+   select 1, 2, 3 into mycols;
+ end;
+ $$ language plpgsql;
+ 
+ drop table somecols;
+ 
+ reset plpgsql.extra_warnings;
#2Pavel Stehule
pavel.stehule@gmail.com
In reply to: Marko Tiikkaja (#1)
Re: plpgsql.extra_warnings='num_into_expressions'

Hi

I looked on this patch and I am thinking so it is not a good idea. It
introduce early dependency between functions and pg_class based objects.

This check should not be integrated to function validation directly.

We can integrate it to plpgsql_check

Regards

Pavel

2014-07-21 22:56 GMT+02:00 Marko Tiikkaja <marko@joh.to>:

Show quoted text

Hi again,

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

.marko

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Marko Tiikkaja
marko@joh.to
In reply to: Pavel Stehule (#2)
Re: plpgsql.extra_warnings='num_into_expressions'

On 7/22/14, 7:06 AM, Pavel Stehule wrote:

I looked on this patch and I am thinking so it is not a good idea. It
introduce early dependency between functions and pg_class based objects.

What dependency? The patch only looks at the raw parser output, so it
won't e.g. know whether SELECT * INTO a, b FROM foo; is problematic or
not.

.marko

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Pavel Stehule
pavel.stehule@gmail.com
In reply to: Marko Tiikkaja (#3)
Re: plpgsql.extra_warnings='num_into_expressions'

2014-07-22 8:52 GMT+02:00 Marko Tiikkaja <marko@joh.to>:

On 7/22/14, 7:06 AM, Pavel Stehule wrote:

I looked on this patch and I am thinking so it is not a good idea. It
introduce early dependency between functions and pg_class based objects.

What dependency? The patch only looks at the raw parser output, so it
won't e.g. know whether SELECT * INTO a, b FROM foo; is problematic or
not.

I am sorry, I was confused

There is dependencty in variable type, but this dependency is not new.

Regards

Pavel

Show quoted text

.marko

#5Marko Tiikkaja
marko@joh.to
In reply to: Marko Tiikkaja (#1)
1 attachment(s)
Re: plpgsql.extra_warnings='num_into_expressions'

On 7/21/14, 10:56 PM, I wrote:

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

New version, fixed bugs with set operations and VALUES lists.

.marko

Attachments:

plpgsql_num_into_expressions_v1.patchtext/plain; charset=UTF-8; name=plpgsql_num_into_expressions_v1.patch; x-mac-creator=0; x-mac-type=0Download
*** a/doc/src/sgml/plpgsql.sgml
--- b/doc/src/sgml/plpgsql.sgml
***************
*** 4719,4725 **** a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
    <varname>plpgsql.extra_errors</> for errors. Both can be set either to
    a comma-separated list of checks, <literal>"none"</> or <literal>"all"</>.
    The default is <literal>"none"</>. Currently the list of available checks
!   includes only one:
    <variablelist>
     <varlistentry>
      <term><varname>shadowed_variables</varname></term>
--- 4719,4725 ----
    <varname>plpgsql.extra_errors</> for errors. Both can be set either to
    a comma-separated list of checks, <literal>"none"</> or <literal>"all"</>.
    The default is <literal>"none"</>. Currently the list of available checks
!   is as follows:
    <variablelist>
     <varlistentry>
      <term><varname>shadowed_variables</varname></term>
***************
*** 4729,4734 **** a_output := a_output || $$ if v_$$ || referrer_keys.kind || $$ like '$$
--- 4729,4744 ----
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><varname>num_into_expressions</varname></term>
+     <listitem>
+      <para>
+       When possible, checks that the number of expressions specified in the
+       SELECT or RETURNING list of a query matches the number expected by the
+       INTO target.  This check is done on a "best effort" basis.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
  
    The following example shows the effect of <varname>plpgsql.extra_warnings</>
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
***************
*** 22,27 ****
--- 22,28 ----
  #include "parser/scanner.h"
  #include "parser/scansup.h"
  #include "utils/builtins.h"
+ #include "nodes/nodefuncs.h"
  
  
  /* Location tracking support --- simpler than bison's default */
***************
*** 97,103 **** static	PLpgSQL_row		*make_scalar_list1(char *initial_name,
  										   PLpgSQL_datum *initial_datum,
  										   int lineno, int location);
  static	void			 check_sql_expr(const char *stmt, int location,
! 										int leaderlen);
  static	void			 plpgsql_sql_error_callback(void *arg);
  static	PLpgSQL_type	*parse_datatype(const char *string, int location);
  static	void			 check_labels(const char *start_label,
--- 98,104 ----
  										   PLpgSQL_datum *initial_datum,
  										   int lineno, int location);
  static	void			 check_sql_expr(const char *stmt, int location,
! 										int leaderlen, PLpgSQL_row *check_row);
  static	void			 plpgsql_sql_error_callback(void *arg);
  static	PLpgSQL_type	*parse_datatype(const char *string, int location);
  static	void			 check_labels(const char *start_label,
***************
*** 106,111 **** static	void			 check_labels(const char *start_label,
--- 107,115 ----
  static	PLpgSQL_expr	*read_cursor_args(PLpgSQL_var *cursor,
  										  int until, const char *expected);
  static	List			*read_raise_options(void);
+ static	bool			find_a_star_walker(Node *node, void *context);
+ static	int				tlist_result_column_count(Node *stmt);
+ 
  
  %}
  
***************
*** 1438,1444 **** for_control		: for_variable K_IN
  								PLpgSQL_stmt_fori	*new;
  
  								/* Check first expression is well-formed */
! 								check_sql_expr(expr1->query, expr1loc, 7);
  
  								/* Read and check the second one */
  								expr2 = read_sql_expression2(K_LOOP, K_BY,
--- 1442,1448 ----
  								PLpgSQL_stmt_fori	*new;
  
  								/* Check first expression is well-formed */
! 								check_sql_expr(expr1->query, expr1loc, 7, NULL);
  
  								/* Read and check the second one */
  								expr2 = read_sql_expression2(K_LOOP, K_BY,
***************
*** 1500,1506 **** for_control		: for_variable K_IN
  								pfree(expr1->query);
  								expr1->query = tmp_query;
  
! 								check_sql_expr(expr1->query, expr1loc, 0);
  
  								new = palloc0(sizeof(PLpgSQL_stmt_fors));
  								new->cmd_type = PLPGSQL_STMT_FORS;
--- 1504,1510 ----
  								pfree(expr1->query);
  								expr1->query = tmp_query;
  
! 								check_sql_expr(expr1->query, expr1loc, 0, NULL);
  
  								new = palloc0(sizeof(PLpgSQL_stmt_fors));
  								new->cmd_type = PLPGSQL_STMT_FORS;
***************
*** 2592,2598 **** read_sql_construct(int until,
  	pfree(ds.data);
  
  	if (valid_sql)
! 		check_sql_expr(expr->query, startlocation, strlen(sqlstart));
  
  	return expr;
  }
--- 2596,2602 ----
  	pfree(ds.data);
  
  	if (valid_sql)
! 		check_sql_expr(expr->query, startlocation, strlen(sqlstart), NULL);
  
  	return expr;
  }
***************
*** 2815,2821 **** make_execsql_stmt(int firsttoken, int location)
  	expr->ns			= plpgsql_ns_top();
  	pfree(ds.data);
  
! 	check_sql_expr(expr->query, location, 0);
  
  	execsql = palloc(sizeof(PLpgSQL_stmt_execsql));
  	execsql->cmd_type = PLPGSQL_STMT_EXECSQL;
--- 2819,2825 ----
  	expr->ns			= plpgsql_ns_top();
  	pfree(ds.data);
  
! 	check_sql_expr(expr->query, location, 0, row);
  
  	execsql = palloc(sizeof(PLpgSQL_stmt_execsql));
  	execsql->cmd_type = PLPGSQL_STMT_EXECSQL;
***************
*** 3409,3421 **** make_scalar_list1(char *initial_name,
   * (typically "SELECT ") prefixed to the source text.  We use this assumption
   * to transpose any error cursor position back to the function source text.
   * If no error cursor is provided, we'll just point at "location".
   */
  static void
! check_sql_expr(const char *stmt, int location, int leaderlen)
  {
  	sql_error_callback_arg cbarg;
  	ErrorContextCallback  syntax_errcontext;
  	MemoryContext oldCxt;
  
  	if (!plpgsql_check_syntax)
  		return;
--- 3413,3430 ----
   * (typically "SELECT ") prefixed to the source text.  We use this assumption
   * to transpose any error cursor position back to the function source text.
   * If no error cursor is provided, we'll just point at "location".
+  *
+  * If check_row is not NULL and PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS is enabled,
+  * the statement's result column count is compared against it when the exact
+  * number of columns is known and an error or a warning is reported.
   */
  static void
! check_sql_expr(const char *stmt, int location, int leaderlen, PLpgSQL_row *check_row)
  {
  	sql_error_callback_arg cbarg;
  	ErrorContextCallback  syntax_errcontext;
  	MemoryContext oldCxt;
+ 	List *raw_parsetree_list;
  
  	if (!plpgsql_check_syntax)
  		return;
***************
*** 3429,3441 **** check_sql_expr(const char *stmt, int location, int leaderlen)
  	error_context_stack = &syntax_errcontext;
  
  	oldCxt = MemoryContextSwitchTo(compile_tmp_cxt);
! 	(void) raw_parser(stmt);
  	MemoryContextSwitchTo(oldCxt);
  
  	/* Restore former ereport callback */
  	error_context_stack = syntax_errcontext.previous;
  }
  
  static void
  plpgsql_sql_error_callback(void *arg)
  {
--- 3438,3535 ----
  	error_context_stack = &syntax_errcontext;
  
  	oldCxt = MemoryContextSwitchTo(compile_tmp_cxt);
! 	raw_parsetree_list = raw_parser(stmt);
! 
! 	if (check_row != NULL &&
! 		(plpgsql_curr_compile->extra_warnings & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS ||
! 		 plpgsql_curr_compile->extra_errors & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS))
! 	{
! 		Node *raw_parse_tree;
! 		int ncols;
! 		int fnum;
! 		int expected_ncols = 0;
! 
! 		for (fnum = 0; fnum < check_row->nfields; fnum++)
! 		{
! 			if (check_row->varnos[fnum] < 0)
! 				continue;
! 			expected_ncols++;
! 		}
! 
! 		raw_parse_tree = linitial(raw_parsetree_list);
! 		ncols = tlist_result_column_count(raw_parse_tree);
! 		if (ncols >= 0 && ncols != expected_ncols)
! 			ereport(plpgsql_curr_compile->extra_errors & PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS ? ERROR : WARNING,
! 				(errcode(ERRCODE_SYNTAX_ERROR),
! 				errmsg("the number of result expressions does not match that expected by the INTO target"),
! 				errdetail("INTO target expects %d expressions, but the query returns %d", expected_ncols, ncols)));
! 	}
  	MemoryContextSwitchTo(oldCxt);
  
  	/* Restore former ereport callback */
  	error_context_stack = syntax_errcontext.previous;
  }
  
+ /*
+  * Expression tree walker for tlist_result_column_count.  Returns true if the
+  * expression tree contains an A_Star node, false otherwise.
+  */
+ static bool
+ find_a_star_walker(Node *node, void *context)
+ {
+ 	if (node == NULL)
+ 		return false;
+ 	if (IsA(node, A_Star))
+ 		return true;
+ 	if (IsA(node, ColumnRef))
+ 	{
+ 		ColumnRef *ref = (ColumnRef *) node;
+ 		/* A_Star can only be the last element */
+ 		if (IsA(llast(ref->fields), A_Star))
+ 			return true;
+ 	}
+ 	return raw_expression_tree_walker((Node *) node,
+ 									  find_a_star_walker,
+ 									  context);
+ }
+ 
+ /*
+  * Find the number of columns in a raw statement's target list (if SELECT) or
+  * returningList (if INSERT, UPDATE or DELETE).  Returns -1 if the number of
+  * columns could not be determined because of an A_Star.
+  */
+ static int
+ tlist_result_column_count(Node *stmt)
+ {
+ 	List *tlist;
+ 
+ 	if (IsA(stmt, SelectStmt))
+ 	{
+ 		SelectStmt *s = (SelectStmt *) stmt;
+ 		if (s->valuesLists)
+ 			tlist = (List *) linitial(s->valuesLists);
+ 		else if (s->op == SETOP_NONE)
+ 			tlist = s->targetList;
+ 		else
+ 			return tlist_result_column_count((Node *) s->larg);
+ 	}
+ 	else if (IsA(stmt, InsertStmt))
+ 		tlist = ((InsertStmt *) stmt)->returningList;
+ 	else if (IsA(stmt, UpdateStmt))
+ 		tlist = ((UpdateStmt *) stmt)->returningList;
+ 	else if (IsA(stmt, DeleteStmt))
+ 		tlist = ((DeleteStmt *) stmt)->returningList;
+ 	else
+ 		elog(ERROR, "unknown nodeTag %d", nodeTag(stmt));
+ 
+ 	if (tlist == NIL)
+ 		return 0;
+ 
+ 	if (raw_expression_tree_walker((Node *) tlist, find_a_star_walker, NULL))
+ 		return -1;
+ 	return list_length(tlist);
+ }
+ 
  static void
  plpgsql_sql_error_callback(void *arg)
  {
*** a/src/pl/plpgsql/src/pl_handler.c
--- b/src/pl/plpgsql/src/pl_handler.c
***************
*** 87,92 **** plpgsql_extra_checks_check_hook(char **newvalue, void **extra, GucSource source)
--- 87,94 ----
  
  			if (pg_strcasecmp(tok, "shadowed_variables") == 0)
  				extrachecks |= PLPGSQL_XCHECK_SHADOWVAR;
+ 			else if (pg_strcasecmp(tok, "num_into_expressions") == 0)
+ 				extrachecks |= PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS;
  			else if (pg_strcasecmp(tok, "all") == 0 || pg_strcasecmp(tok, "none") == 0)
  			{
  				GUC_check_errdetail("Key word \"%s\" cannot be combined with other key words.", tok);
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
***************
*** 886,894 **** extern int	plpgsql_variable_conflict;
  extern bool plpgsql_print_strict_params;
  
  /* extra compile-time checks */
! #define PLPGSQL_XCHECK_NONE			0
! #define PLPGSQL_XCHECK_SHADOWVAR	1
! #define PLPGSQL_XCHECK_ALL			((int) ~0)
  
  extern int	plpgsql_extra_warnings;
  extern int	plpgsql_extra_errors;
--- 886,895 ----
  extern bool plpgsql_print_strict_params;
  
  /* extra compile-time checks */
! #define PLPGSQL_XCHECK_NONE						0
! #define PLPGSQL_XCHECK_SHADOWVAR				1
! #define PLPGSQL_XCHECK_NUM_INTO_EXPRESSIONS		2
! #define PLPGSQL_XCHECK_ALL						((int) ~0)
  
  extern int	plpgsql_extra_warnings;
  extern int	plpgsql_extra_errors;
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
***************
*** 5340,5342 **** NOTICE:  outer_func() done
--- 5340,5404 ----
  drop function outer_outer_func(int);
  drop function outer_func(int);
  drop function inner_func(int);
+ create temporary table somecols(a int, b int);
+ set plpgsql.extra_warnings to 'num_into_expressions';
+ -- INTO column counts
+ create or replace function into_counts()
+ returns void as $$
+ declare
+   myresult int;
+   myrec record;
+   mycols somecols;
+ begin
+   -- no warning
+   select 1 into myresult;
+   -- we don't know the column count without parse analysis, no warning
+   select * into myresult from somecols;
+   -- records are OK
+   select 1, 2 into myrec;
+   -- warning
+   select a, b into myresult from somecols;
+   -- warning
+   select into myresult from somecols;
+   -- no warning
+   select 1, 2 into mycols;
+   -- warning
+   select 1 into mycols;
+   -- warning
+   select 1, 2, 3 into mycols;
+   -- set operations; OK
+   select 1 into myresult union all select 2;
+   -- set operations; not OK
+   select 1,2 into myresult union all select 2,3;
+   -- values; OK
+   values (1), (2) into myresult;
+   -- values; not OK
+   values (1,2),(2,3) into myresult;
+ end;
+ $$ language plpgsql;
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 15:   select a, b into myresult from somecols;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 2
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 17:   select into myresult from somecols;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 0
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 21:   select 1 into mycols;
+            ^
+ DETAIL:  INTO target expects 2 expressions, but the query returns 1
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 23:   select 1, 2, 3 into mycols;
+            ^
+ DETAIL:  INTO target expects 2 expressions, but the query returns 3
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 27:   select 1,2 into myresult union all select 2,3;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 2
+ WARNING:  the number of result expressions does not match that expected by the INTO target
+ LINE 31:   values (1,2),(2,3) into myresult;
+            ^
+ DETAIL:  INTO target expects 1 expressions, but the query returns 2
+ drop table somecols;
+ reset plpgsql.extra_warnings;
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
***************
*** 4193,4195 **** select outer_outer_func(20);
--- 4193,4238 ----
  drop function outer_outer_func(int);
  drop function outer_func(int);
  drop function inner_func(int);
+ 
+ create temporary table somecols(a int, b int);
+ 
+ set plpgsql.extra_warnings to 'num_into_expressions';
+ 
+ -- INTO column counts
+ create or replace function into_counts()
+ returns void as $$
+ declare
+   myresult int;
+   myrec record;
+   mycols somecols;
+ begin
+   -- no warning
+   select 1 into myresult;
+   -- we don't know the column count without parse analysis, no warning
+   select * into myresult from somecols;
+   -- records are OK
+   select 1, 2 into myrec;
+   -- warning
+   select a, b into myresult from somecols;
+   -- warning
+   select into myresult from somecols;
+   -- no warning
+   select 1, 2 into mycols;
+   -- warning
+   select 1 into mycols;
+   -- warning
+   select 1, 2, 3 into mycols;
+   -- set operations; OK
+   select 1 into myresult union all select 2;
+   -- set operations; not OK
+   select 1,2 into myresult union all select 2,3;
+   -- values; OK
+   values (1), (2) into myresult;
+   -- values; not OK
+   values (1,2),(2,3) into myresult;
+ end;
+ $$ language plpgsql;
+ 
+ drop table somecols;
+ 
+ reset plpgsql.extra_warnings;
#6Heikki Linnakangas
hlinnakangas@vmware.com
In reply to: Marko Tiikkaja (#5)
Re: plpgsql.extra_warnings='num_into_expressions'

On 08/07/2014 01:11 AM, Marko Tiikkaja wrote:

On 7/21/14, 10:56 PM, I wrote:

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

New version, fixed bugs with set operations and VALUES lists.

Looks good.

It seems weird to pass a PLpgSQL_row struct to check_sql_expr.
check_sql_expr only needs to know how many attributes is expected to be
in the target list, so it would be more natural to just pass an "int
expected_natts".

Once you do that, you could trivially also add checking for other cases,
e.g:

do $$
declare
i int4;
begin
-- fails at runtime, because "SELECT 1,3" returns two attributes,
-- but FOR expects 1
for i in 1,3..(2) loop
raise notice 'foo %', i;
end loop;
end;
$$;

There's probably more checking like that that you could add, but that
can be done as add-on patches, if ever. The INTO mistake happens a lot
more easily.

- Heikki

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Marko Tiikkaja
marko@joh.to
In reply to: Heikki Linnakangas (#6)
Re: plpgsql.extra_warnings='num_into_expressions'

On 8/21/14, 1:19 PM, Heikki Linnakangas wrote:

On 08/07/2014 01:11 AM, Marko Tiikkaja wrote:

On 7/21/14, 10:56 PM, I wrote:

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

New version, fixed bugs with set operations and VALUES lists.

Looks good.

There's probably more checking like that that you could add, but that
can be done as add-on patches, if ever. The INTO mistake happens a lot
more easily.

Yeah, I think the mistake is at least as easy to do in "FOR .. IN
<query>", and I'm planning to add checks for that as well. But I
haven't found the time to look at it amongst all the other patches and
projects I have going (and also, unfortunately, I'm on vacation right now).

It seems weird to pass a PLpgSQL_row struct to check_sql_expr.
check_sql_expr only needs to know how many attributes is expected to be
in the target list, so it would be more natural to just pass an "int
expected_natts".

I'm not sure about this, though. AFAICT all the interesting cases are
already holding a PLpgSQL_row, and in that case it seems easier to just
pass that in to check_sql_expr() without making the callers worry about
extracting the expected_natts from the row. And we can always change
the interface should such a case come up, since the interface is
completely internal. Just my 0.02EUR, of course.

.marko

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Heikki Linnakangas
hlinnakangas@vmware.com
In reply to: Marko Tiikkaja (#7)
Re: plpgsql.extra_warnings='num_into_expressions'

On 08/21/2014 02:09 PM, Marko Tiikkaja wrote:

On 8/21/14, 1:19 PM, Heikki Linnakangas wrote:

On 08/07/2014 01:11 AM, Marko Tiikkaja wrote:

On 7/21/14, 10:56 PM, I wrote:

Here's a patch which allows you to notice those annoying bugs with INTO
slightly more quickly. Adding to the next commit phest.

New version, fixed bugs with set operations and VALUES lists.

Looks good.

There's probably more checking like that that you could add, but that
can be done as add-on patches, if ever. The INTO mistake happens a lot
more easily.

Yeah, I think the mistake is at least as easy to do in "FOR .. IN
<query>", and I'm planning to add checks for that as well. But I
haven't found the time to look at it amongst all the other patches and
projects I have going

Ok.

(and also, unfortunately, I'm on vacation right now).

Oh, have fun!

It seems weird to pass a PLpgSQL_row struct to check_sql_expr.
check_sql_expr only needs to know how many attributes is expected to be
in the target list, so it would be more natural to just pass an "int
expected_natts".

I'm not sure about this, though. AFAICT all the interesting cases are
already holding a PLpgSQL_row, and in that case it seems easier to just
pass that in to check_sql_expr() without making the callers worry about
extracting the expected_natts from the row.

Hmm. The integer FOR syntax I used in my example does not, it always
expects 1 output column.

And we can always change
the interface should such a case come up, since the interface is
completely internal. Just my 0.02EUR, of course.

You might want to add a helper function to count the number of
attributes in a PLpgSQL_row. Then the check_sql_expr call would be
almost as simple: check_sql_expr(..., get_row_natts(row)).

- Heikki

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers