From 4fb92718be654b38652dfe893d075f3862e84eae Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Thu, 23 Mar 2006 04:22:37 +0000
Subject: [PATCH] Fix plpgsql to pass only one copy of any given plpgsql
 variable into a SQL command or expression, rather than one copy for each
 textual occurrence as it did before.  This might result in some small
 performance improvement, but the compelling reason to do it is that not doing
 so can result in unexpected grouping failures because the main SQL parser
 won't see different parameter numbers as equivalent.  Add a regression test
 for the failure case. Per report from Robert Davidson.

---
 src/pl/plpgsql/src/gram.y             | 92 +++++++++++++++++----------
 src/test/regress/expected/plpgsql.out | 18 ++++++
 src/test/regress/sql/plpgsql.sql      | 15 +++++
 3 files changed, 92 insertions(+), 33 deletions(-)

diff --git a/src/pl/plpgsql/src/gram.y b/src/pl/plpgsql/src/gram.y
index 8636a0a6b85..7364e89b949 100644
--- a/src/pl/plpgsql/src/gram.y
+++ b/src/pl/plpgsql/src/gram.y
@@ -9,7 +9,7 @@
  *
  *
  * IDENTIFICATION
- *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.87 2006/03/09 21:29:36 momjian Exp $
+ *	  $PostgreSQL: pgsql/src/pl/plpgsql/src/gram.y,v 1.88 2006/03/23 04:22:36 tgl Exp $
  *
  *-------------------------------------------------------------------------
  */
@@ -1714,6 +1714,44 @@ lno				:
 %%
 
 
+#define MAX_EXPR_PARAMS  1024
+
+/*
+ * determine the expression parameter position to use for a plpgsql datum
+ *
+ * It is important that any given plpgsql datum map to just one parameter.
+ * We used to be sloppy and assign a separate parameter for each occurrence
+ * of a datum reference, but that fails for situations such as "select DATUM
+ * from ... group by DATUM".
+ *
+ * The params[] array must be of size MAX_EXPR_PARAMS.
+ */
+static int
+assign_expr_param(int dno, int *params, int *nparams)
+{
+	int		i;
+
+	/* already have an instance of this dno? */
+	for (i = 0; i < *nparams; i++)
+	{
+		if (params[i] == dno)
+			return i+1;
+	}
+	/* check for array overflow */
+	if (*nparams >= MAX_EXPR_PARAMS)
+	{
+		plpgsql_error_lineno = plpgsql_scanner_lineno();
+		ereport(ERROR,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("too many variables specified in SQL statement")));
+	}
+	/* add new parameter dno to array */
+	params[*nparams] = dno;
+	(*nparams)++;
+	return *nparams;
+}
+
+
 PLpgSQL_expr *
 plpgsql_read_expression(int until, const char *expected)
 {
@@ -1752,7 +1790,7 @@ read_sql_construct(int until,
 	PLpgSQL_dstring		ds;
 	int					parenlevel = 0;
 	int					nparams = 0;
-	int					params[1024];
+	int					params[MAX_EXPR_PARAMS];
 	char				buf[32];
 	PLpgSQL_expr		*expr;
 
@@ -1804,32 +1842,26 @@ read_sql_construct(int until,
 		if (plpgsql_SpaceScanned)
 			plpgsql_dstring_append(&ds, " ");
 
-		/* Check for array overflow */
-		if (nparams >= 1024)
-		{
-			plpgsql_error_lineno = lno;
-			ereport(ERROR,
-					(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-					 errmsg("too many variables specified in SQL statement")));
-		}
-
 		switch (tok)
 		{
 			case T_SCALAR:
-				params[nparams] = yylval.scalar->dno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.scalar->dno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
 			case T_ROW:
-				params[nparams] = yylval.row->rowno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.row->rowno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
 			case T_RECORD:
-				params[nparams] = yylval.rec->recno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.rec->recno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
@@ -1927,7 +1959,7 @@ make_select_stmt(void)
 {
 	PLpgSQL_dstring		ds;
 	int					nparams = 0;
-	int					params[1024];
+	int					params[MAX_EXPR_PARAMS];
 	char				buf[32];
 	PLpgSQL_expr		*expr;
 	PLpgSQL_row			*row = NULL;
@@ -1992,32 +2024,26 @@ make_select_stmt(void)
 		if (plpgsql_SpaceScanned)
 			plpgsql_dstring_append(&ds, " ");
 
-		/* Check for array overflow */
-		if (nparams >= 1024)
-		{
-			plpgsql_error_lineno = plpgsql_scanner_lineno();
-			ereport(ERROR,
-					(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
-					 errmsg("too many parameters specified in SQL statement")));
-		}
-
 		switch (tok)
 		{
 			case T_SCALAR:
-				params[nparams] = yylval.scalar->dno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.scalar->dno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
 			case T_ROW:
-				params[nparams] = yylval.row->rowno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.row->rowno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
 			case T_RECORD:
-				params[nparams] = yylval.rec->recno;
-				snprintf(buf, sizeof(buf), " $%d ", ++nparams);
+				snprintf(buf, sizeof(buf), " $%d ",
+						 assign_expr_param(yylval.rec->recno,
+										   params, &nparams));
 				plpgsql_dstring_append(&ds, buf);
 				break;
 
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index e72383bbb72..6e6597dadb2 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2776,3 +2776,21 @@ NOTICE:  4 bb cc
  
 (1 row)
 
+-- regression test: verify that multiple uses of same plpgsql datum within
+-- a SQL command all get mapped to the same $n parameter.  The return value
+-- of the SELECT is not important, we only care that it doesn't fail with
+-- a complaint about an ungrouped column reference.
+create function multi_datum_use(p1 int) returns bool as $$
+declare
+  x int;
+  y int;
+begin
+  select into x,y unique1/p1, unique1/$1 from tenk1 group by unique1/p1;
+  return x = y;
+end$$ language plpgsql;
+select multi_datum_use(42);
+ multi_datum_use 
+-----------------
+ t
+(1 row)
+
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index a2d65817b1f..19e145be65f 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -2309,3 +2309,18 @@ end;
 $proc$ language plpgsql;
 
 select for_vect();
+
+-- regression test: verify that multiple uses of same plpgsql datum within
+-- a SQL command all get mapped to the same $n parameter.  The return value
+-- of the SELECT is not important, we only care that it doesn't fail with
+-- a complaint about an ungrouped column reference.
+create function multi_datum_use(p1 int) returns bool as $$
+declare
+  x int;
+  y int;
+begin
+  select into x,y unique1/p1, unique1/$1 from tenk1 group by unique1/p1;
+  return x = y;
+end$$ language plpgsql;
+
+select multi_datum_use(42);
-- 
GitLab