From 36f0dc29c1187c49c5072d48796ccbb3369ed8f7 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 3 Dec 2019 10:08:35 -0300
Subject: [PATCH v18] Add appendStringInfoStringQuoted
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Simplifies some coding that prints parameters, as well as optimize to do
it per non-quote chunks instead of per byte.

This version of the patch makes the new function available to frontends,
and puts wchar.c/encnames.c in libpgport.

Author: Alexey Bashtanov and Álvaro Herrera, after a suggestion from Andres Freund
Discussion: https://postgr.es/m/20190920203905.xkv5udsd5dxfs6tr@alap3.anarazel.de
---
 src/backend/tcop/postgres.c           | 11 +----
 src/backend/utils/mb/mbutils.c        | 31 ------------
 src/backend/utils/mb/wchar.c          | 47 ++++++++++++++++++
 src/common/Makefile                   |  9 +++-
 src/common/stringinfo.c               | 71 +++++++++++++++++++++++++++
 src/include/lib/stringinfo.h          | 10 ++++
 src/pl/plpgsql/src/pl_exec.c          | 39 +++++----------
 src/test/regress/expected/plpgsql.out | 14 ++++++
 src/test/regress/sql/plpgsql.sql      | 13 +++++
 9 files changed, 178 insertions(+), 67 deletions(-)

diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 3b85e48333..a5e398b2f5 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -2348,7 +2348,6 @@ errdetail_params(ParamListInfo params)
 			Oid			typoutput;
 			bool		typisvarlena;
 			char	   *pstring;
-			char	   *p;
 
 			appendStringInfo(&param_str, "%s$%d = ",
 							 paramno > 0 ? ", " : "",
@@ -2364,14 +2363,8 @@ errdetail_params(ParamListInfo params)
 
 			pstring = OidOutputFunctionCall(typoutput, prm->value);
 
-			appendStringInfoCharMacro(&param_str, '\'');
-			for (p = pstring; *p; p++)
-			{
-				if (*p == '\'') /* double single quotes */
-					appendStringInfoCharMacro(&param_str, *p);
-				appendStringInfoCharMacro(&param_str, *p);
-			}
-			appendStringInfoCharMacro(&param_str, '\'');
+			appendStringInfoStringQuoted(&param_str, GetDatabaseEncoding(),
+										 pstring, 0);
 
 			pfree(pstring);
 		}
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 6b08b77717..34e9fd9f2c 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -865,37 +865,6 @@ pg_mbcliplen(const char *mbstr, int len, int limit)
 								 len, limit);
 }
 
-/*
- * pg_mbcliplen with specified encoding
- */
-int
-pg_encoding_mbcliplen(int encoding, const char *mbstr,
-					  int len, int limit)
-{
-	mblen_converter mblen_fn;
-	int			clen = 0;
-	int			l;
-
-	/* optimization for single byte encoding */
-	if (pg_encoding_max_length(encoding) == 1)
-		return cliplen(mbstr, len, limit);
-
-	mblen_fn = pg_wchar_table[encoding].mblen;
-
-	while (len > 0 && *mbstr)
-	{
-		l = (*mblen_fn) ((const unsigned char *) mbstr);
-		if ((clen + l) > limit)
-			break;
-		clen += l;
-		if (clen == limit)
-			break;
-		len -= l;
-		mbstr += l;
-	}
-	return clen;
-}
-
 /*
  * Similar to pg_mbcliplen except the limit parameter specifies the
  * character length, not the byte length.
diff --git a/src/backend/utils/mb/wchar.c b/src/backend/utils/mb/wchar.c
index b2d598cbee..f0e57593f7 100644
--- a/src/backend/utils/mb/wchar.c
+++ b/src/backend/utils/mb/wchar.c
@@ -14,6 +14,10 @@
 #include "mb/pg_wchar.h"
 
 
+/* Internal functions */
+static int	cliplen(const char *str, int len, int limit);
+
+
 /*
  * Operations on multi-byte encodings are driven by a table of helper
  * functions.
@@ -1861,6 +1865,49 @@ pg_encoding_verifymb(int encoding, const char *mbstr, int len)
 			pg_wchar_table[PG_SQL_ASCII].mbverify((const unsigned char *) mbstr, len));
 }
 
+/* mbcliplen for any single-byte encoding */
+static int
+cliplen(const char *str, int len, int limit)
+{
+	int			l = 0;
+
+	len = Min(len, limit);
+	while (l < len && str[l])
+		l++;
+	return l;
+}
+
+/*
+ * pg_mbcliplen with specified encoding
+ */
+int
+pg_encoding_mbcliplen(int encoding, const char *mbstr,
+					  int len, int limit)
+{
+	mblen_converter mblen_fn;
+	int			clen = 0;
+	int			l;
+
+	/* optimization for single byte encoding */
+	if (pg_encoding_max_length(encoding) == 1)
+		return cliplen(mbstr, len, limit);
+
+	mblen_fn = pg_wchar_table[encoding].mblen;
+
+	while (len > 0 && *mbstr)
+	{
+		l = (*mblen_fn) ((const unsigned char *) mbstr);
+		if ((clen + l) > limit)
+			break;
+		clen += l;
+		if (clen == limit)
+			break;
+		len -= l;
+		mbstr += l;
+	}
+	return clen;
+}
+
 /*
  * fetch maximum length of a given encoding
  */
diff --git a/src/common/Makefile b/src/common/Makefile
index ffb0f6edff..0146918758 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -78,6 +78,8 @@ else
 OBJS_COMMON += sha2.o
 endif
 
+backend_src = $(top_srcdir)/src/backend
+
 # A few files are currently only built for frontend, not server
 # (Mkvcbuild.pm has a copy of this list, too)
 OBJS_FRONTEND = \
@@ -85,7 +87,9 @@ OBJS_FRONTEND = \
 	fe_memutils.o \
 	file_utils.o \
 	logging.o \
-	restricted_token.o
+	restricted_token.o \
+	wchar.o \
+	encnames.o
 
 # foo.o, foo_shlib.o, and foo_srv.o are all built from foo.c
 OBJS_SHLIB = $(OBJS_FRONTEND:%.o=%_shlib.o)
@@ -158,6 +162,9 @@ kwlist_d.h: $(top_srcdir)/src/include/parser/kwlist.h $(GEN_KEYWORDLIST_DEPS)
 # that you don't get broken parsing code, even in a non-enable-depend build.
 keywords.o keywords_shlib.o keywords_srv.o: kwlist_d.h
 
+encnames.c wchar.c: % : $(backend_src)/utils/mb/%
+	rm -f $@ && $(LN_S) $< .
+
 # The code imported from Ryu gets a pass on declaration-after-statement,
 # in order to keep it more closely aligned with its upstream.
 RYU_FILES = d2s.o f2s.o
diff --git a/src/common/stringinfo.c b/src/common/stringinfo.c
index a50e587da9..5198ef97ac 100644
--- a/src/common/stringinfo.c
+++ b/src/common/stringinfo.c
@@ -30,6 +30,7 @@
 #endif
 
 #include "lib/stringinfo.h"
+#include "mb/pg_wchar.h"
 
 
 /*
@@ -178,6 +179,76 @@ appendStringInfoString(StringInfo str, const char *s)
 	appendBinaryStringInfo(str, s, strlen(s));
 }
 
+/*
+ * appendStringInfoStringQuoted
+ *
+ * Append up to maxlen characters from s (which is in the given encoding) to
+ * str, or the whole input string if maxlen <= 0, adding single quotes around
+ * it and doubling all single quotes.  Add an ellipsis if the copy is
+ * incomplete.
+ */
+void
+appendStringInfoStringQuoted(StringInfo str, int encoding, const char *s,
+							 int maxlen)
+{
+	char	   *copy = NULL;
+	const char *chunk_search_start,
+			   *chunk_copy_start,
+			   *chunk_end;
+	bool		ellipsis;
+	int			slen;
+
+	Assert(str != NULL);
+
+	slen = strlen(s);
+	if (maxlen > 0 && maxlen < slen)
+	{
+		int		finallen = pg_encoding_mbcliplen(encoding, s, slen, maxlen);
+
+		copy = pnstrdup(s, finallen);
+		chunk_search_start = copy;
+		chunk_copy_start = copy;
+
+		ellipsis = true;
+	}
+	else
+	{
+		chunk_search_start = s;
+		chunk_copy_start = s;
+
+		ellipsis = false;
+	}
+
+	appendStringInfoCharMacro(str, '\'');
+
+	while ((chunk_end = strchr(chunk_search_start, '\'')) != NULL)
+	{
+		/* copy including the found delimiting ' */
+		appendBinaryStringInfoNT(str,
+								 chunk_copy_start,
+								 chunk_end - chunk_copy_start + 1);
+
+		/* in order to double it, include this ' into the next chunk as well */
+		chunk_copy_start = chunk_end;
+		chunk_search_start = chunk_end + 1;
+	}
+
+	/* copy the last chunk and terminate */
+	if (ellipsis)
+		appendStringInfo(str, "%s...'", chunk_copy_start);
+	else
+	{
+		int		len = strlen(chunk_copy_start);
+
+		/* ensure sufficient space for terminators */
+		appendBinaryStringInfoNT(str, chunk_copy_start, len);
+		appendStringInfoCharMacro(str, '\'');
+	}
+
+	if (copy)
+		pfree(copy);
+}
+
 /*
  * appendStringInfoChar
  *
diff --git a/src/include/lib/stringinfo.h b/src/include/lib/stringinfo.h
index e27942728e..4a2af9e50b 100644
--- a/src/include/lib/stringinfo.h
+++ b/src/include/lib/stringinfo.h
@@ -113,6 +113,16 @@ extern int	appendStringInfoVA(StringInfo str, const char *fmt, va_list args) pg_
  */
 extern void appendStringInfoString(StringInfo str, const char *s);
 
+/*------------------------
+ * appendStringInfoStringQuoted
+ * Append up to maxlen characters from s (which is in the given encoding) to
+ * str, or the whole input string if maxlen <= 0, adding single quotes around
+ * it and doubling all single quotes.  Add an ellipsis if the copy is
+ * incomplete.
+ */
+extern void appendStringInfoStringQuoted(StringInfo str, int encoding,
+										 const char *s, int maxlen);
+
 /*------------------------
  * appendStringInfoChar
  * Append a single byte to str.
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 4f0de7a811..1ba1783490 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -28,6 +28,7 @@
 #include "executor/spi.h"
 #include "executor/spi_priv.h"
 #include "funcapi.h"
+#include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/optimizer.h"
@@ -8611,19 +8612,12 @@ format_expr_params(PLpgSQL_execstate *estate,
 		if (paramisnull)
 			appendStringInfoString(&paramstr, "NULL");
 		else
-		{
-			char	   *value = convert_value_to_string(estate, paramdatum, paramtypeid);
-			char	   *p;
-
-			appendStringInfoCharMacro(&paramstr, '\'');
-			for (p = value; *p; p++)
-			{
-				if (*p == '\'') /* double single quotes */
-					appendStringInfoCharMacro(&paramstr, *p);
-				appendStringInfoCharMacro(&paramstr, *p);
-			}
-			appendStringInfoCharMacro(&paramstr, '\'');
-		}
+			appendStringInfoStringQuoted(&paramstr,
+										 GetDatabaseEncoding(),
+										 convert_value_to_string(estate,
+																 paramdatum,
+																 paramtypeid),
+										 0);
 
 		paramno++;
 	}
@@ -8661,19 +8655,12 @@ format_preparedparamsdata(PLpgSQL_execstate *estate,
 		if (ppd->nulls[paramno] == 'n')
 			appendStringInfoString(&paramstr, "NULL");
 		else
-		{
-			char	   *value = convert_value_to_string(estate, ppd->values[paramno], ppd->types[paramno]);
-			char	   *p;
-
-			appendStringInfoCharMacro(&paramstr, '\'');
-			for (p = value; *p; p++)
-			{
-				if (*p == '\'') /* double single quotes */
-					appendStringInfoCharMacro(&paramstr, *p);
-				appendStringInfoCharMacro(&paramstr, *p);
-			}
-			appendStringInfoCharMacro(&paramstr, '\'');
-		}
+			appendStringInfoStringQuoted(&paramstr,
+										 GetDatabaseEncoding(),
+										 convert_value_to_string(estate,
+																 ppd->values[paramno],
+																 ppd->types[paramno]),
+										 0);
 	}
 
 	MemoryContextSwitchTo(oldcontext);
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index e85b29455e..cd2c79f4d5 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -2656,6 +2656,20 @@ create or replace function stricttest() returns void as $$
 declare
 x record;
 p1 int := 2;
+p3 text := $a$'Valame Dios!' dijo Sancho; 'no le dije yo a vuestra merced que mirase bien lo que hacia?'$a$;
+begin
+  -- no rows
+  select * from foo where f1 = p1 and f1::text = p3 into strict x;
+  raise notice 'x.f1 = %, x.f2 = %', x.f1, x.f2;
+end$$ language plpgsql;
+select stricttest();
+ERROR:  query returned no rows
+DETAIL:  parameters: p1 = '2', p3 = '''Valame Dios!'' dijo Sancho; ''no le dije yo a vuestra merced que mirase bien lo que hacia?'''
+CONTEXT:  PL/pgSQL function stricttest() line 8 at SQL statement
+create or replace function stricttest() returns void as $$
+declare
+x record;
+p1 int := 2;
 p3 text := 'foo';
 begin
   -- too many rows
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 70deadfbea..d841d8c0f9 100644
--- a/src/test/regress/sql/plpgsql.sql
+++ b/src/test/regress/sql/plpgsql.sql
@@ -2280,6 +2280,19 @@ end$$ language plpgsql;
 
 select stricttest();
 
+create or replace function stricttest() returns void as $$
+declare
+x record;
+p1 int := 2;
+p3 text := $a$'Valame Dios!' dijo Sancho; 'no le dije yo a vuestra merced que mirase bien lo que hacia?'$a$;
+begin
+  -- no rows
+  select * from foo where f1 = p1 and f1::text = p3 into strict x;
+  raise notice 'x.f1 = %, x.f2 = %', x.f1, x.f2;
+end$$ language plpgsql;
+
+select stricttest();
+
 create or replace function stricttest() returns void as $$
 declare
 x record;
-- 
2.20.1

