On Tue, 2026-08-11 at 14:31 -0400, Andres Freund wrote:
> Hm. If I infer the pg_strlower() API correctly - it's utterly
> underdocumented

Agreed. Patch attached.

> I'd also make i size_t, given that the input is size_t.  Perhaps
> practically
> no problem, but I see no reason to not use size_t here.

Patch attached for that, too.

I also attached patches to make all the functions work with
collate_is_c, and fixed up the -1 API in 18.

Regards,
        Jeff Davis

From 3dabc8c4ccc9b4b0d6dbe8f87353f511d6862bb4 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Thu, 13 Aug 2026 20:57:20 -0700
Subject: [PATCH vPG18 1/4] Fixup 5f003855e7 for srclen < 0.

No actual problem because no callers used that aspect of the API.

Only commit to 18, because that part of the API was removed in commit
6d22c67c3b.

Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
---
 src/backend/utils/adt/pg_locale.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 2f8d5fea8f2..9c721efb52f 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1328,6 +1328,8 @@ size_t
 pg_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
 {
+	srclen = (srclen < 0) ? strlen(src) : srclen;
+
 	if (locale->ctype_is_c)
 		return strlower_c(dst, dstsize, src, srclen);
 	else if (locale->provider == COLLPROVIDER_BUILTIN)
@@ -1349,6 +1351,8 @@ size_t
 pg_strtitle(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
 {
+	srclen = (srclen < 0) ? strlen(src) : srclen;
+
 	if (locale->ctype_is_c)
 		return strtitle_c(dst, dstsize, src, srclen);
 	else if (locale->provider == COLLPROVIDER_BUILTIN)
@@ -1370,6 +1374,8 @@ size_t
 pg_strupper(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
 {
+	srclen = (srclen < 0) ? strlen(src) : srclen;
+
 	if (locale->ctype_is_c)
 		return strupper_c(dst, dstsize, src, srclen);
 	else if (locale->provider == COLLPROVIDER_BUILTIN)
@@ -1391,6 +1397,8 @@ size_t
 pg_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 		   pg_locale_t locale)
 {
+	srclen = (srclen < 0) ? strlen(src) : srclen;
+
 	/* in the C locale, casefolding is the same as lowercasing */
 	if (locale->ctype_is_c)
 		return strlower_c(dst, dstsize, src, srclen);
-- 
2.43.0

From 6f11163f52b320f1b4ef1cb66a06a73f210de46f Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Thu, 13 Aug 2026 21:12:34 -0700
Subject: [PATCH vPG18 2/4] pg_locale.c, unicode_case.c: use size_t for
 iteration.

No actual problem, just cleanup. Only relevant to 18 and 19.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
---
 src/backend/utils/adt/pg_locale.c | 6 +++---
 src/common/unicode_case.c         | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 9c721efb52f..8e79ccbd9f6 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1277,7 +1277,7 @@ get_collation_actual_version(char collprovider, const char *collcollate)
 static size_t
 strlower_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 		dst[i] = pg_ascii_tolower(src[i]);
@@ -1291,7 +1291,7 @@ static size_t
 strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
 	bool		wasalnum = false;
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 	{
@@ -1315,7 +1315,7 @@ strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 static size_t
 strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 		dst[i] = pg_ascii_toupper(src[i]);
diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c
index 8639b203e0c..86e0b57d7d3 100644
--- a/src/common/unicode_case.c
+++ b/src/common/unicode_case.c
@@ -341,7 +341,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset)
 	int			ulen;
 
 	/* iterate backwards looking for preceding character */
-	for (int i = offset; i > 0;)
+	for (size_t i = offset; i > 0;)
 	{
 		/* skip backwards through continuation bytes */
 		i--;
@@ -369,7 +369,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset)
 	ulen = utf8_mblen((const unsigned char *) str + offset);
 
 	/* iterate forward looking for following character */
-	for (int i = offset + ulen; i < len;)
+	for (size_t i = offset + ulen; i < len;)
 	{
 		ulen = utf8_mblen((const unsigned char *) str + i);
 
-- 
2.43.0

From 70ebdd061f9ae158b1674453e4a6e03db1b74815 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:32:19 -0700
Subject: [PATCH vPG18 3/4] Add missing comments in pg_locale.c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v3nniwcrxejmcfvz56xbd22hphprqleuornd6hqkmw2bl7kgmz@cnytz2ee5ltk
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 76 +++++++++++++++++++++++++------
 1 file changed, 63 insertions(+), 13 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 8e79ccbd9f6..c21619f85cd 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1324,6 +1324,20 @@ strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 	return srclen;
 }
 
+/*
+ * pg_strlower()
+ *
+ * Convert src to lowercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If srclen is
+ * -1, src must be NUL-terminated.  If dstsize is zero, dst may be NULL, which
+ * is useful for calculating the required buffer size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
@@ -1347,6 +1361,20 @@ pg_strlower(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * pg_strtitle()
+ *
+ * Convert src to titlecase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If srclen is
+ * -1, src must be NUL-terminated.  If dstsize is zero, dst may be NULL, which
+ * is useful for calculating the required buffer size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strtitle(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
@@ -1370,6 +1398,20 @@ pg_strtitle(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * pg_strupper()
+ *
+ * Convert src to uppercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If srclen is
+ * -1, src must be NUL-terminated.  If dstsize is zero, dst may be NULL, which
+ * is useful for calculating the required buffer size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strupper(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
@@ -1393,6 +1435,19 @@ pg_strupper(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * pg_strfold()
+ *
+ * Casefold src, and return the result length (not including terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If srclen is
+ * -1, src must be NUL-terminated.  If dstsize is zero, dst may be NULL, which
+ * is useful for calculating the required buffer size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 		   pg_locale_t locale)
@@ -1432,12 +1487,10 @@ pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 /*
  * pg_strncoll
  *
- * Call ucol_strcollUTF8(), ucol_strcoll(), strcoll_l() or wcscoll_l() as
- * appropriate for the given locale, platform, and database encoding. If the
- * locale is not specified, use the database collation.
+ * Compare strings according to the given locale.
  *
- * The input strings must be encoded in the database encoding. If an input
- * string is NUL-terminated, its length may be specified as -1.
+ * Strings must be encoded in the database encoding with no embedded NULs.  If
+ * an input string is NUL-terminated, its length may be specified as -1.
  *
  * The caller is responsible for breaking ties if the collation is
  * deterministic; this maintains consistency with pg_strnxfrm(), which cannot
@@ -1453,9 +1506,6 @@ pg_strncoll(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2,
 /*
  * Return true if the collation provider supports pg_strxfrm() and
  * pg_strnxfrm(); otherwise false.
- *
- *
- * No similar problem is known for the ICU provider.
  */
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
@@ -1486,9 +1536,9 @@ pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
  * ordinary strcmp() on transformed strings is equivalent to pg_strcoll() on
  * untransformed strings.
  *
- * The input string must be encoded in the database encoding. If the input
- * string is NUL-terminated, its length may be specified as -1. If 'destsize'
- * is zero, 'dest' may be NULL.
+ * String must be encoded in the database encoding with no embedded NULs.  If
+ * srclen is -1, src must be NUL-terminated.  If 'destsize' is zero, 'dest'
+ * may be NULL.
  *
  * Not all providers support pg_strnxfrm() safely. The caller should check
  * pg_strxfrm_enabled() first, otherwise this function may return wrong
@@ -1534,8 +1584,8 @@ pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
  * memcmp() on the byte sequence is equivalent to pg_strncoll() on
  * untransformed strings. The result is not nul-terminated.
  *
- * The input string must be encoded in the database encoding. If the input
- * string is NUL-terminated, its length may be specified as -1.
+ * String must be encoded in the database encoding with no embedded NULs. If
+ * the input string is NUL-terminated, its length may be specified as -1.
  *
  * Not all providers support pg_strnxfrm_prefix() safely. The caller should
  * check pg_strxfrm_prefix_enabled() first, otherwise this function may return
-- 
2.43.0

From 27e336a2add2660fa28ba410dc8888aac7be9fb3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:46:48 -0700
Subject: [PATCH vPG18 4/4] Ensure all pg_locale.h APIs work with collate_is_c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 64 ++++++++++++++++++++++++++++---
 1 file changed, 58 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index c21619f85cd..ad9f416ec13 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1481,7 +1481,10 @@ pg_strfold(char *dst, size_t dstsize, const char *src, ssize_t srclen,
 int
 pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 {
-	return locale->collate->strncoll(arg1, -1, arg2, -1, locale);
+	if (locale->collate == NULL)
+		return strcmp(arg1, arg2);
+	else
+		return locale->collate->strncoll(arg1, -1, arg2, -1, locale);
 }
 
 /*
@@ -1500,7 +1503,20 @@ int
 pg_strncoll(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2,
 			pg_locale_t locale)
 {
-	return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
+	if (locale->collate == NULL)
+	{
+		int			result;
+
+		len1 = (len1 < 0) ? strlen(arg1) : len1;
+		len2 = (len2 < 0) ? strlen(arg2) : len2;
+		result = memcmp(arg1, arg2, Min(len1, len2));
+
+		if ((result == 0) && (len1 != len2))
+			result = (len1 < len2) ? -1 : 1;
+		return result;
+	}
+	else
+		return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
 }
 
 /*
@@ -1510,6 +1526,9 @@ pg_strncoll(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2,
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+		return true;
+
 	/*
 	 * locale->collate->strnxfrm is still a required method, even if it may
 	 * have the wrong behavior, because the planner uses it for estimates in
@@ -1526,7 +1545,10 @@ pg_strxfrm_enabled(pg_locale_t locale)
 size_t
 pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
 {
-	return locale->collate->strnxfrm(dest, destsize, src, -1, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strnxfrm(dest, destsize, src, -1, locale);
 }
 
 /*
@@ -1552,6 +1574,18 @@ size_t
 pg_strnxfrm(char *dest, size_t destsize, const char *src, ssize_t srclen,
 			pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+	{
+		srclen = (srclen < 0) ? strlen(src) : srclen;
+
+		if (destsize > srclen)
+		{
+			memcpy(dest, src, srclen);
+			dest[srclen] = '\0';
+		}
+
+		return srclen;
+	}
 	return locale->collate->strnxfrm(dest, destsize, src, srclen, locale);
 }
 
@@ -1562,7 +1596,10 @@ pg_strnxfrm(char *dest, size_t destsize, const char *src, ssize_t srclen,
 bool
 pg_strxfrm_prefix_enabled(pg_locale_t locale)
 {
-	return (locale->collate->strnxfrm_prefix != NULL);
+	if (locale->collate == NULL)
+		return true;
+	else
+		return (locale->collate->strnxfrm_prefix != NULL);
 }
 
 /*
@@ -1574,7 +1611,10 @@ size_t
 pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
 				  pg_locale_t locale)
 {
-	return locale->collate->strnxfrm_prefix(dest, destsize, src, -1, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm_prefix(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strnxfrm_prefix(dest, destsize, src, -1, locale);
 }
 
 /*
@@ -1599,7 +1639,19 @@ size_t
 pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src,
 				   ssize_t srclen, pg_locale_t locale)
 {
-	return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
+	if (locale->collate == NULL)
+	{
+		size_t		len;
+
+		srclen = (srclen < 0) ? strlen(src) : srclen;
+		len = Min(srclen, destsize);
+
+		if (destsize > 0)
+			memcpy(dest, src, len);
+		return len;
+	}
+	else
+		return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
 }
 
 /*
-- 
2.43.0

From c1a532d38ae63f27ff705a6bbdec78ca5aed5b6f Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Thu, 13 Aug 2026 21:12:34 -0700
Subject: [PATCH vPG19 1/3] pg_locale.c, unicode_case.c: use size_t for
 iteration.

No actual problem, just cleanup. Only relevant to 18 and 19.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
---
 src/backend/utils/adt/pg_locale.c | 6 +++---
 src/common/unicode_case.c         | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 11d48a3916e..9eb99487e57 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1270,7 +1270,7 @@ get_collation_actual_version(char collprovider, const char *collcollate)
 static size_t
 strlower_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 		dst[i] = pg_ascii_tolower(src[i]);
@@ -1284,7 +1284,7 @@ static size_t
 strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
 	bool		wasalnum = false;
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 	{
@@ -1308,7 +1308,7 @@ strtitle_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 static size_t
 strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 {
-	int			i;
+	size_t		i;
 
 	for (i = 0; i < srclen && i < dstsize; i++)
 		dst[i] = pg_ascii_toupper(src[i]);
diff --git a/src/common/unicode_case.c b/src/common/unicode_case.c
index dd5b3ba86d0..744b9116b12 100644
--- a/src/common/unicode_case.c
+++ b/src/common/unicode_case.c
@@ -336,7 +336,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset)
 	int			ulen;
 
 	/* iterate backwards looking for preceding character */
-	for (int i = offset; i > 0;)
+	for (size_t i = offset; i > 0;)
 	{
 		/* skip backwards through continuation bytes */
 		i--;
@@ -364,7 +364,7 @@ check_final_sigma(const unsigned char *str, size_t len, size_t offset)
 	ulen = utf8_mblen((const unsigned char *) str + offset);
 
 	/* iterate forward looking for following character */
-	for (int i = offset + ulen; i < len;)
+	for (size_t i = offset + ulen; i < len;)
 	{
 		ulen = utf8_mblen((const unsigned char *) str + i);
 
-- 
2.43.0

From 81ad1161ed864e1572e5319eecec037670e940b4 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:32:19 -0700
Subject: [PATCH vPG19 2/3] Add missing comments in pg_locale.c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v3nniwcrxejmcfvz56xbd22hphprqleuornd6hqkmw2bl7kgmz@cnytz2ee5ltk
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 70 ++++++++++++++++++++++++++-----
 1 file changed, 60 insertions(+), 10 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 9eb99487e57..da32590396f 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1317,6 +1317,20 @@ strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 	return srclen;
 }
 
+/*
+ * pg_strlower()
+ *
+ * Convert src to lowercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strlower(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1327,6 +1341,20 @@ pg_strlower(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strlower(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strtitle()
+ *
+ * Convert src to titlecase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strtitle(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1337,6 +1365,20 @@ pg_strtitle(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strtitle(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strupper()
+ *
+ * Convert src to uppercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strupper(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1347,6 +1389,19 @@ pg_strupper(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strupper(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strfold()
+ *
+ * Casefold src, and return the result length (not including terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strfold(char *dst, size_t dstsize, const char *src, size_t srclen,
 		   pg_locale_t locale)
@@ -1392,11 +1447,9 @@ pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 /*
  * pg_strncoll
  *
- * Call ucol_strcollUTF8(), ucol_strcoll(), strcoll_l() or wcscoll_l() as
- * appropriate for the given locale, platform, and database encoding. If the
- * locale is not specified, use the database collation.
+ * Compare strings according to the given locale.
  *
- * The input strings must be encoded in the database encoding.
+ * Strings must be encoded in the database encoding with no embedded NULs.
  *
  * The caller is responsible for breaking ties if the collation is
  * deterministic; this maintains consistency with pg_strnxfrm(), which cannot
@@ -1412,9 +1465,6 @@ pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 /*
  * Return true if the collation provider supports pg_strxfrm() and
  * pg_strnxfrm(); otherwise false.
- *
- *
- * No similar problem is known for the ICU provider.
  */
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
@@ -1445,8 +1495,8 @@ pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
  * ordinary strcmp() on transformed strings is equivalent to pg_strcoll() on
  * untransformed strings.
  *
- * The input string must be encoded in the database encoding. If 'destsize' is
- * zero, 'dest' may be NULL.
+ * String must be encoded in the database encoding with no embedded NULs.  If
+ * 'destsize' is zero, 'dest' may be NULL.
  *
  * Not all providers support pg_strnxfrm() safely. The caller should check
  * pg_strxfrm_enabled() first, otherwise this function may return wrong
@@ -1492,7 +1542,7 @@ pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
  * memcmp() on the byte sequence is equivalent to pg_strncoll() on
  * untransformed strings. The result is not nul-terminated.
  *
- * The input string must be encoded in the database encoding.
+ * String must be encoded in the database encoding with no embedded NULs.
  *
  * Not all providers support pg_strnxfrm_prefix() safely. The caller should
  * check pg_strxfrm_prefix_enabled() first, otherwise this function may return
-- 
2.43.0

From acf021658ccc38e01bda2fb9900c1d0ecacf1535 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:46:48 -0700
Subject: [PATCH vPG19 3/3] Ensure all pg_locale.h APIs work with collate_is_c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 55 +++++++++++++++++++++++++++----
 1 file changed, 49 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index da32590396f..2b501a7b6b7 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1441,7 +1441,10 @@ pg_downcase_ident(char *dst, size_t dstsize, const char *src, size_t srclen)
 int
 pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 {
-	return locale->collate->strcoll(arg1, arg2, locale);
+	if (locale->collate == NULL)
+		return strcmp(arg1, arg2);
+	else
+		return locale->collate->strcoll(arg1, arg2, locale);
 }
 
 /*
@@ -1459,7 +1462,16 @@ int
 pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 			pg_locale_t locale)
 {
-	return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
+	if (locale->collate == NULL)
+	{
+		int			result = memcmp(arg1, arg2, Min(len1, len2));
+
+		if ((result == 0) && (len1 != len2))
+			result = (len1 < len2) ? -1 : 1;
+		return result;
+	}
+	else
+		return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
 }
 
 /*
@@ -1469,6 +1481,9 @@ pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+		return true;
+
 	/*
 	 * locale->collate->strnxfrm is still a required method, even if it may
 	 * have the wrong behavior, because the planner uses it for estimates in
@@ -1485,7 +1500,10 @@ pg_strxfrm_enabled(pg_locale_t locale)
 size_t
 pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
 {
-	return locale->collate->strxfrm(dest, destsize, src, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strxfrm(dest, destsize, src, locale);
 }
 
 /*
@@ -1510,6 +1528,16 @@ size_t
 pg_strnxfrm(char *dest, size_t destsize, const char *src, size_t srclen,
 			pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+	{
+		if (destsize > srclen)
+		{
+			memcpy(dest, src, srclen);
+			dest[srclen] = '\0';
+		}
+
+		return srclen;
+	}
 	return locale->collate->strnxfrm(dest, destsize, src, srclen, locale);
 }
 
@@ -1520,7 +1548,10 @@ pg_strnxfrm(char *dest, size_t destsize, const char *src, size_t srclen,
 bool
 pg_strxfrm_prefix_enabled(pg_locale_t locale)
 {
-	return (locale->collate->strnxfrm_prefix != NULL);
+	if (locale->collate == NULL)
+		return true;
+	else
+		return (locale->collate->strnxfrm_prefix != NULL);
 }
 
 /*
@@ -1532,7 +1563,10 @@ size_t
 pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
 				  pg_locale_t locale)
 {
-	return locale->collate->strxfrm_prefix(dest, destsize, src, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm_prefix(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strxfrm_prefix(dest, destsize, src, locale);
 }
 
 /*
@@ -1556,7 +1590,16 @@ size_t
 pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src,
 				   size_t srclen, pg_locale_t locale)
 {
-	return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
+	if (locale->collate == NULL)
+	{
+		size_t		len = Min(srclen, destsize);
+
+		if (destsize > 0)
+			memcpy(dest, src, len);
+		return len;
+	}
+	else
+		return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
 }
 
 bool
-- 
2.43.0

From daaea4c338afb1c1389ffbfa6ac33d0bc38a5b48 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:32:19 -0700
Subject: [PATCH vPG20 1/2] Add missing comments in pg_locale.c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v3nniwcrxejmcfvz56xbd22hphprqleuornd6hqkmw2bl7kgmz@cnytz2ee5ltk
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 70 ++++++++++++++++++++++++++-----
 1 file changed, 60 insertions(+), 10 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 9eb99487e57..da32590396f 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1317,6 +1317,20 @@ strupper_c(char *dst, size_t dstsize, const char *src, size_t srclen)
 	return srclen;
 }
 
+/*
+ * pg_strlower()
+ *
+ * Convert src to lowercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strlower(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1327,6 +1341,20 @@ pg_strlower(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strlower(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strtitle()
+ *
+ * Convert src to titlecase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strtitle(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1337,6 +1365,20 @@ pg_strtitle(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strtitle(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strupper()
+ *
+ * Convert src to uppercase, and return the result length (not including
+ * terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strupper(char *dst, size_t dstsize, const char *src, size_t srclen,
 			pg_locale_t locale)
@@ -1347,6 +1389,19 @@ pg_strupper(char *dst, size_t dstsize, const char *src, size_t srclen,
 		return locale->ctype->strupper(dst, dstsize, src, srclen, locale);
 }
 
+/*
+ * pg_strfold()
+ *
+ * Casefold src, and return the result length (not including terminating NUL).
+ *
+ * src must be in the database encoding with no embedded NULs.  If dstsize is
+ * zero, dst may be NULL, which is useful for calculating the required buffer
+ * size before allocating.
+ *
+ * If the result length is less than dstsize, the NUL-terminated result is
+ * stored in dst.  Otherwise, the contents of dst are undefined, and the
+ * caller should use the return value to resize the buffer and retry.
+ */
 size_t
 pg_strfold(char *dst, size_t dstsize, const char *src, size_t srclen,
 		   pg_locale_t locale)
@@ -1392,11 +1447,9 @@ pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 /*
  * pg_strncoll
  *
- * Call ucol_strcollUTF8(), ucol_strcoll(), strcoll_l() or wcscoll_l() as
- * appropriate for the given locale, platform, and database encoding. If the
- * locale is not specified, use the database collation.
+ * Compare strings according to the given locale.
  *
- * The input strings must be encoded in the database encoding.
+ * Strings must be encoded in the database encoding with no embedded NULs.
  *
  * The caller is responsible for breaking ties if the collation is
  * deterministic; this maintains consistency with pg_strnxfrm(), which cannot
@@ -1412,9 +1465,6 @@ pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 /*
  * Return true if the collation provider supports pg_strxfrm() and
  * pg_strnxfrm(); otherwise false.
- *
- *
- * No similar problem is known for the ICU provider.
  */
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
@@ -1445,8 +1495,8 @@ pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
  * ordinary strcmp() on transformed strings is equivalent to pg_strcoll() on
  * untransformed strings.
  *
- * The input string must be encoded in the database encoding. If 'destsize' is
- * zero, 'dest' may be NULL.
+ * String must be encoded in the database encoding with no embedded NULs.  If
+ * 'destsize' is zero, 'dest' may be NULL.
  *
  * Not all providers support pg_strnxfrm() safely. The caller should check
  * pg_strxfrm_enabled() first, otherwise this function may return wrong
@@ -1492,7 +1542,7 @@ pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
  * memcmp() on the byte sequence is equivalent to pg_strncoll() on
  * untransformed strings. The result is not nul-terminated.
  *
- * The input string must be encoded in the database encoding.
+ * String must be encoded in the database encoding with no embedded NULs.
  *
  * Not all providers support pg_strnxfrm_prefix() safely. The caller should
  * check pg_strxfrm_prefix_enabled() first, otherwise this function may return
-- 
2.43.0

From 7fbcfeeeb04ecefe917c29e53237d79d4447795e Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 12 Aug 2026 07:46:48 -0700
Subject: [PATCH vPG20 2/2] Ensure all pg_locale.h APIs work with collate_is_c.

Suggested-by: Andres Freund <[email protected]>
Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/backend/utils/adt/pg_locale.c | 55 +++++++++++++++++++++++++++----
 1 file changed, 49 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index da32590396f..2b501a7b6b7 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -1441,7 +1441,10 @@ pg_downcase_ident(char *dst, size_t dstsize, const char *src, size_t srclen)
 int
 pg_strcoll(const char *arg1, const char *arg2, pg_locale_t locale)
 {
-	return locale->collate->strcoll(arg1, arg2, locale);
+	if (locale->collate == NULL)
+		return strcmp(arg1, arg2);
+	else
+		return locale->collate->strcoll(arg1, arg2, locale);
 }
 
 /*
@@ -1459,7 +1462,16 @@ int
 pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 			pg_locale_t locale)
 {
-	return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
+	if (locale->collate == NULL)
+	{
+		int			result = memcmp(arg1, arg2, Min(len1, len2));
+
+		if ((result == 0) && (len1 != len2))
+			result = (len1 < len2) ? -1 : 1;
+		return result;
+	}
+	else
+		return locale->collate->strncoll(arg1, len1, arg2, len2, locale);
 }
 
 /*
@@ -1469,6 +1481,9 @@ pg_strncoll(const char *arg1, size_t len1, const char *arg2, size_t len2,
 bool
 pg_strxfrm_enabled(pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+		return true;
+
 	/*
 	 * locale->collate->strnxfrm is still a required method, even if it may
 	 * have the wrong behavior, because the planner uses it for estimates in
@@ -1485,7 +1500,10 @@ pg_strxfrm_enabled(pg_locale_t locale)
 size_t
 pg_strxfrm(char *dest, const char *src, size_t destsize, pg_locale_t locale)
 {
-	return locale->collate->strxfrm(dest, destsize, src, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strxfrm(dest, destsize, src, locale);
 }
 
 /*
@@ -1510,6 +1528,16 @@ size_t
 pg_strnxfrm(char *dest, size_t destsize, const char *src, size_t srclen,
 			pg_locale_t locale)
 {
+	if (locale->collate == NULL)
+	{
+		if (destsize > srclen)
+		{
+			memcpy(dest, src, srclen);
+			dest[srclen] = '\0';
+		}
+
+		return srclen;
+	}
 	return locale->collate->strnxfrm(dest, destsize, src, srclen, locale);
 }
 
@@ -1520,7 +1548,10 @@ pg_strnxfrm(char *dest, size_t destsize, const char *src, size_t srclen,
 bool
 pg_strxfrm_prefix_enabled(pg_locale_t locale)
 {
-	return (locale->collate->strnxfrm_prefix != NULL);
+	if (locale->collate == NULL)
+		return true;
+	else
+		return (locale->collate->strnxfrm_prefix != NULL);
 }
 
 /*
@@ -1532,7 +1563,10 @@ size_t
 pg_strxfrm_prefix(char *dest, const char *src, size_t destsize,
 				  pg_locale_t locale)
 {
-	return locale->collate->strxfrm_prefix(dest, destsize, src, locale);
+	if (locale->collate == NULL)
+		return pg_strnxfrm_prefix(dest, destsize, src, strlen(src), locale);
+	else
+		return locale->collate->strxfrm_prefix(dest, destsize, src, locale);
 }
 
 /*
@@ -1556,7 +1590,16 @@ size_t
 pg_strnxfrm_prefix(char *dest, size_t destsize, const char *src,
 				   size_t srclen, pg_locale_t locale)
 {
-	return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
+	if (locale->collate == NULL)
+	{
+		size_t		len = Min(srclen, destsize);
+
+		if (destsize > 0)
+			memcpy(dest, src, len);
+		return len;
+	}
+	else
+		return locale->collate->strnxfrm_prefix(dest, destsize, src, srclen, locale);
 }
 
 bool
-- 
2.43.0

Reply via email to