On Thu, 2026-08-13 at 22:17 -0700, Jeff Davis wrote:
> 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.

Now with a C test module (made with AI assistance).

I plan to start committing these fairly soon. I'm not sure whether to
backport the C test module, but I included the patches to do so.

Regards,
        Jeff Davis

From 77e5ec7124d4530fead31794cfee62905d7254d2 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/5] 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 f669e9fafb9bf435c946dcc2c2dd98efb4aa46cb 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/5] 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 a8e72bb34c979ac43ee8480fd10ed4e73db47b10 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/5] 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 a578ea2245f071f3ba7897ce3063c8926ba765b5 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/5] 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 adeef54179a11f5b7817054bc53f8b7b584d2b6a Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Sat, 15 Aug 2026 12:26:09 -0700
Subject: [PATCH vPG18 5/5] Add C test module for pg_locale.h APIs.

Test the API independently to account for fallback paths that aren't
adequately tested from SQL.

The backport to 18 also tests the previously-supported behavior where
a size of -1 meant that the string was NUL-terminated. That behavior
was later removed in 19.

Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_pg_locale/.gitignore    |   4 +
 src/test/modules/test_pg_locale/Makefile      |  23 +++
 src/test/modules/test_pg_locale/README        |   2 +
 .../expected/test_pg_locale.out               |  38 ++++
 src/test/modules/test_pg_locale/meson.build   |  33 ++++
 .../test_pg_locale/sql/test_pg_locale.sql     |  23 +++
 .../test_pg_locale/test_pg_locale--1.0.sql    |   8 +
 .../modules/test_pg_locale/test_pg_locale.c   | 169 ++++++++++++++++++
 .../test_pg_locale/test_pg_locale.control     |   4 +
 11 files changed, 306 insertions(+)
 create mode 100644 src/test/modules/test_pg_locale/.gitignore
 create mode 100644 src/test/modules/test_pg_locale/Makefile
 create mode 100644 src/test/modules/test_pg_locale/README
 create mode 100644 src/test/modules/test_pg_locale/expected/test_pg_locale.out
 create mode 100644 src/test/modules/test_pg_locale/meson.build
 create mode 100644 src/test/modules/test_pg_locale/sql/test_pg_locale.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.c
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4e82d6f1517..9ee74b66797 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -33,6 +33,7 @@ SUBDIRS = \
 		  test_oat_hooks \
 		  test_parser \
 		  test_pg_dump \
+		  test_pg_locale \
 		  test_predtest \
 		  test_radixtree \
 		  test_rbtree \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 9a957351ab6..85ef25deadf 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -32,6 +32,7 @@ subdir('test_misc')
 subdir('test_oat_hooks')
 subdir('test_parser')
 subdir('test_pg_dump')
+subdir('test_pg_locale')
 subdir('test_predtest')
 subdir('test_radixtree')
 subdir('test_rbtree')
diff --git a/src/test/modules/test_pg_locale/.gitignore b/src/test/modules/test_pg_locale/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_pg_locale/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_pg_locale/Makefile b/src/test/modules/test_pg_locale/Makefile
new file mode 100644
index 00000000000..9b051f8a697
--- /dev/null
+++ b/src/test/modules/test_pg_locale/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_pg_locale/Makefile
+
+MODULE_big = test_pg_locale
+OBJS = \
+	$(WIN32RES) \
+	test_pg_locale.o
+PGFILEDESC = "test_pg_locale - test code for pg_locale.h APIs"
+
+EXTENSION = test_pg_locale
+DATA = test_pg_locale--1.0.sql
+
+REGRESS = test_pg_locale
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_pg_locale
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_pg_locale/README b/src/test/modules/test_pg_locale/README
new file mode 100644
index 00000000000..d95af97c005
--- /dev/null
+++ b/src/test/modules/test_pg_locale/README
@@ -0,0 +1,2 @@
+Calls pg_locale.h wrappers directly.  Ordinary SQL tests do not reach
+the C-locale fallbacks because in-tree callers special-case collate_is_c.
diff --git a/src/test/modules/test_pg_locale/expected/test_pg_locale.out b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
new file mode 100644
index 00000000000..edbba284552
--- /dev/null
+++ b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
@@ -0,0 +1,38 @@
+CREATE EXTENSION test_pg_locale;
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_pg_locale/meson.build b/src/test/modules/test_pg_locale/meson.build
new file mode 100644
index 00000000000..f5097464ad2
--- /dev/null
+++ b/src/test/modules/test_pg_locale/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_pg_locale_sources = files(
+  'test_pg_locale.c',
+)
+
+if host_system == 'windows'
+  test_pg_locale_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_pg_locale',
+    '--FILEDESC', 'test_pg_locale - test code for pg_locale.h APIs',])
+endif
+
+test_pg_locale = shared_module('test_pg_locale',
+  test_pg_locale_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_pg_locale
+
+test_install_data += files(
+  'test_pg_locale.control',
+  'test_pg_locale--1.0.sql',
+)
+
+tests += {
+  'name': 'test_pg_locale',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_pg_locale',
+    ],
+  },
+}
diff --git a/src/test/modules/test_pg_locale/sql/test_pg_locale.sql b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
new file mode 100644
index 00000000000..212012feb9c
--- /dev/null
+++ b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
@@ -0,0 +1,23 @@
+CREATE EXTENSION test_pg_locale;
+
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
diff --git a/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
new file mode 100644
index 00000000000..134c4befa06
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_pg_locale/test_pg_locale--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_pg_locale" to load this file. \quit
+
+CREATE FUNCTION test_pg_locale_apis(oid)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.c b/src/test/modules/test_pg_locale/test_pg_locale.c
new file mode 100644
index 00000000000..79041fb3f69
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.c
@@ -0,0 +1,169 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_pg_locale.c
+ *		Call pg_locale.h wrappers directly.
+ *
+ * SQL callers special-case collate_is_c, so the C-locale fallbacks are
+ * not reached by ordinary regression tests.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_pg_locale/test_pg_locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/pg_locale.h"
+
+PG_MODULE_MAGIC;
+
+static void
+test_case_mapping(pg_locale_t locale)
+{
+	char		buf[32];
+	size_t		n;
+
+	n = pg_strlower(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strlower() size probe returned %zu, expected 3", n);
+	n = pg_strlower(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strlower() produced \"%s\"", buf);
+
+	n = pg_strupper(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strupper() size probe returned %zu, expected 3", n);
+	n = pg_strupper(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "ABC") != 0)
+		elog(ERROR, "pg_strupper() produced \"%s\"", buf);
+
+	n = pg_strfold(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strfold() produced \"%s\"", buf);
+
+	buf[0] = '\0';
+	n = pg_strtitle(buf, sizeof(buf), "hello-world", 11, locale);
+	if (n != 11)
+		elog(ERROR, "pg_strtitle() returned %zu, expected 11", n);
+	if (locale->ctype_is_c && strcmp(buf, "Hello-World") != 0)
+		elog(ERROR, "pg_strtitle() produced \"%s\"", buf);
+
+	n = pg_strlower(buf, sizeof(buf), "AbC", -1, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strlower() with srclen -1 produced \"%s\"", buf);
+}
+
+static void
+test_collate(pg_locale_t locale)
+{
+	char		buf[32];
+	char		pfx[8];
+	char		x1[8];
+	char		x2[8];
+	size_t		n;
+
+	if (pg_strcoll("abc", "abc", locale) != 0 ||
+		pg_strncoll("abc", 3, "abc", 3, locale) != 0 ||
+		pg_strcoll("", "", locale) != 0)
+		elog(ERROR, "equal strings did not compare equal");
+
+	if (locale->collate_is_c)
+	{
+		if (locale->collate != NULL)
+			elog(ERROR, "collate_is_c but collate methods are set");
+		if (pg_strcoll("abc", "abd", locale) >= 0 ||
+			pg_strcoll("abd", "abc", locale) <= 0 ||
+			pg_strncoll("ab", 2, "abc", 3, locale) >= 0 ||
+			pg_strncoll("abc", 3, "ab", 2, locale) <= 0 ||
+			pg_strncoll("xyz", 3, "abc", 2, locale) <= 0)
+			elog(ERROR, "C-locale comparison result is wrong");
+
+		if (!pg_strxfrm_enabled(locale))
+			elog(ERROR, "pg_strxfrm_enabled() is false for C locale");
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() size probe returned %zu, expected 3", n);
+		n = pg_strnxfrm(buf, 4, "abc", 3, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strnxfrm() produced \"%s\"", buf);
+		n = pg_strxfrm(buf, "abc", 4, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strxfrm() produced \"%s\"", buf);
+		n = pg_strnxfrm(buf, 3, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() destsize==srclen returned %zu", n);
+		n = pg_strnxfrm(buf, 2, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() short dest returned %zu", n);
+
+		if (!pg_strxfrm_prefix_enabled(locale))
+			elog(ERROR, "pg_strxfrm_prefix_enabled() is false for C locale");
+		n = pg_strnxfrm_prefix(NULL, 0, "abcdef", 6, locale);
+		if (n != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize 0 returned %zu", n);
+		n = pg_strnxfrm_prefix(pfx, 2, "abcdef", 6, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() produced a wrong prefix");
+		n = pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale);
+		if (n != 3 || memcmp(pfx, "abc", 3) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize>=srclen produced a wrong result");
+		n = pg_strxfrm_prefix(pfx, "abcdef", 2, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strxfrm_prefix() produced a wrong prefix");
+
+		if (pg_strxfrm(x1, "abc", sizeof(x1), locale) >= sizeof(x1) ||
+			pg_strxfrm(x2, "abd", sizeof(x2), locale) >= sizeof(x2) ||
+			(strcmp(x1, x2) < 0) != (pg_strcoll("abc", "abd", locale) < 0))
+			elog(ERROR, "pg_strxfrm() disagrees with pg_strcoll()");
+	}
+	else
+	{
+		char	   *tmp;
+
+		if (locale->collate == NULL)
+			elog(ERROR, "collate methods missing for non-C locale");
+
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		tmp = palloc(n + 1);
+		if (pg_strnxfrm(tmp, n + 1, "abc", 3, locale) > n)
+			elog(ERROR, "pg_strnxfrm() grew on the second call");
+		pfree(tmp);
+
+		if (pg_strxfrm_prefix_enabled(locale) &&
+			pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale) > sizeof(pfx))
+			elog(ERROR, "pg_strnxfrm_prefix() exceeded destsize");
+	}
+
+	if (pg_strncoll("abc", -1, "abc", -1, locale) != 0)
+		elog(ERROR, "pg_strncoll() with length -1 failed");
+	if (locale->collate_is_c)
+	{
+		n = pg_strnxfrm(buf, sizeof(buf), "abc", -1, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strnxfrm() with srclen -1 produced \"%s\"", buf);
+		n = pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", -1, locale);
+		if (n != 3 || memcmp(pfx, "abc", 3) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() with srclen -1 produced a wrong result");
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_pg_locale_apis);
+
+Datum
+test_pg_locale_apis(PG_FUNCTION_ARGS)
+{
+	pg_locale_t locale;
+
+	locale = pg_newlocale_from_collation(PG_GETARG_OID(0));
+	if (locale == NULL)
+		elog(ERROR, "pg_newlocale_from_collation() returned NULL");
+
+	test_collate(locale);
+	test_case_mapping(locale);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.control b/src/test/modules/test_pg_locale/test_pg_locale.control
new file mode 100644
index 00000000000..6b224d04a1b
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.control
@@ -0,0 +1,4 @@
+comment = 'Test code for pg_locale.h APIs'
+default_version = '1.0'
+module_pathname = '$libdir/test_pg_locale'
+relocatable = true
-- 
2.43.0

From 3d63edf5d0132614d8007845220517cd6dfd2f37 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/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 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 2d63e62bc454828630f68aa5bfff486a47540bff 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/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 | 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 a93385298c5350b85d2b9033b5ea88d0775e383d 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/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 | 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 d59ba32bd3a72442a49aee71a740784a1087b94b Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Sat, 15 Aug 2026 12:26:09 -0700
Subject: [PATCH vPG19 4/4] Add C test module for pg_locale.h APIs.

Test the API independently to account for fallback paths that aren't
adequately tested from SQL.

The backport to 18 also tests the previously-supported behavior where
a size of -1 meant that the string was NUL-terminated. That behavior
was later removed in 19.

Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_pg_locale/.gitignore    |   4 +
 src/test/modules/test_pg_locale/Makefile      |  23 +++
 src/test/modules/test_pg_locale/README        |   2 +
 .../expected/test_pg_locale.out               |  38 +++++
 src/test/modules/test_pg_locale/meson.build   |  33 ++++
 .../test_pg_locale/sql/test_pg_locale.sql     |  23 +++
 .../test_pg_locale/test_pg_locale--1.0.sql    |   8 +
 .../modules/test_pg_locale/test_pg_locale.c   | 153 ++++++++++++++++++
 .../test_pg_locale/test_pg_locale.control     |   4 +
 11 files changed, 290 insertions(+)
 create mode 100644 src/test/modules/test_pg_locale/.gitignore
 create mode 100644 src/test/modules/test_pg_locale/Makefile
 create mode 100644 src/test/modules/test_pg_locale/README
 create mode 100644 src/test/modules/test_pg_locale/expected/test_pg_locale.out
 create mode 100644 src/test/modules/test_pg_locale/meson.build
 create mode 100644 src/test/modules/test_pg_locale/sql/test_pg_locale.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.c
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 098bb8142ae..8a2b09bd11e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -41,6 +41,7 @@ SUBDIRS = \
 		  test_oat_hooks \
 		  test_parser \
 		  test_pg_dump \
+		  test_pg_locale \
 		  test_plan_advice \
 		  test_predtest \
 		  test_radixtree \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 4bca42bb370..71c4035b1b3 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -42,6 +42,7 @@ subdir('test_misc')
 subdir('test_oat_hooks')
 subdir('test_parser')
 subdir('test_pg_dump')
+subdir('test_pg_locale')
 subdir('test_plan_advice')
 subdir('test_predtest')
 subdir('test_radixtree')
diff --git a/src/test/modules/test_pg_locale/.gitignore b/src/test/modules/test_pg_locale/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_pg_locale/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_pg_locale/Makefile b/src/test/modules/test_pg_locale/Makefile
new file mode 100644
index 00000000000..9b051f8a697
--- /dev/null
+++ b/src/test/modules/test_pg_locale/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_pg_locale/Makefile
+
+MODULE_big = test_pg_locale
+OBJS = \
+	$(WIN32RES) \
+	test_pg_locale.o
+PGFILEDESC = "test_pg_locale - test code for pg_locale.h APIs"
+
+EXTENSION = test_pg_locale
+DATA = test_pg_locale--1.0.sql
+
+REGRESS = test_pg_locale
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_pg_locale
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_pg_locale/README b/src/test/modules/test_pg_locale/README
new file mode 100644
index 00000000000..d95af97c005
--- /dev/null
+++ b/src/test/modules/test_pg_locale/README
@@ -0,0 +1,2 @@
+Calls pg_locale.h wrappers directly.  Ordinary SQL tests do not reach
+the C-locale fallbacks because in-tree callers special-case collate_is_c.
diff --git a/src/test/modules/test_pg_locale/expected/test_pg_locale.out b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
new file mode 100644
index 00000000000..edbba284552
--- /dev/null
+++ b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
@@ -0,0 +1,38 @@
+CREATE EXTENSION test_pg_locale;
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_pg_locale/meson.build b/src/test/modules/test_pg_locale/meson.build
new file mode 100644
index 00000000000..f5097464ad2
--- /dev/null
+++ b/src/test/modules/test_pg_locale/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_pg_locale_sources = files(
+  'test_pg_locale.c',
+)
+
+if host_system == 'windows'
+  test_pg_locale_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_pg_locale',
+    '--FILEDESC', 'test_pg_locale - test code for pg_locale.h APIs',])
+endif
+
+test_pg_locale = shared_module('test_pg_locale',
+  test_pg_locale_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_pg_locale
+
+test_install_data += files(
+  'test_pg_locale.control',
+  'test_pg_locale--1.0.sql',
+)
+
+tests += {
+  'name': 'test_pg_locale',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_pg_locale',
+    ],
+  },
+}
diff --git a/src/test/modules/test_pg_locale/sql/test_pg_locale.sql b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
new file mode 100644
index 00000000000..212012feb9c
--- /dev/null
+++ b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
@@ -0,0 +1,23 @@
+CREATE EXTENSION test_pg_locale;
+
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
diff --git a/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
new file mode 100644
index 00000000000..134c4befa06
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_pg_locale/test_pg_locale--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_pg_locale" to load this file. \quit
+
+CREATE FUNCTION test_pg_locale_apis(oid)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.c b/src/test/modules/test_pg_locale/test_pg_locale.c
new file mode 100644
index 00000000000..122152f5fcb
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.c
@@ -0,0 +1,153 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_pg_locale.c
+ *		Call pg_locale.h wrappers directly.
+ *
+ * SQL callers special-case collate_is_c, so the C-locale fallbacks are
+ * not reached by ordinary regression tests.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_pg_locale/test_pg_locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/pg_locale.h"
+
+PG_MODULE_MAGIC;
+
+static void
+test_case_mapping(pg_locale_t locale)
+{
+	char		buf[32];
+	size_t		n;
+
+	n = pg_strlower(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strlower() size probe returned %zu, expected 3", n);
+	n = pg_strlower(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strlower() produced \"%s\"", buf);
+
+	n = pg_strupper(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strupper() size probe returned %zu, expected 3", n);
+	n = pg_strupper(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "ABC") != 0)
+		elog(ERROR, "pg_strupper() produced \"%s\"", buf);
+
+	n = pg_strfold(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strfold() produced \"%s\"", buf);
+
+	buf[0] = '\0';
+	n = pg_strtitle(buf, sizeof(buf), "hello-world", 11, locale);
+	if (n != 11)
+		elog(ERROR, "pg_strtitle() returned %zu, expected 11", n);
+	if (locale->ctype_is_c && strcmp(buf, "Hello-World") != 0)
+		elog(ERROR, "pg_strtitle() produced \"%s\"", buf);
+}
+
+static void
+test_collate(pg_locale_t locale)
+{
+	char		buf[32];
+	char		pfx[8];
+	char		x1[8];
+	char		x2[8];
+	size_t		n;
+
+	if (pg_strcoll("abc", "abc", locale) != 0 ||
+		pg_strncoll("abc", 3, "abc", 3, locale) != 0 ||
+		pg_strcoll("", "", locale) != 0)
+		elog(ERROR, "equal strings did not compare equal");
+
+	if (locale->collate_is_c)
+	{
+		if (locale->collate != NULL)
+			elog(ERROR, "collate_is_c but collate methods are set");
+		if (pg_strcoll("abc", "abd", locale) >= 0 ||
+			pg_strcoll("abd", "abc", locale) <= 0 ||
+			pg_strncoll("ab", 2, "abc", 3, locale) >= 0 ||
+			pg_strncoll("abc", 3, "ab", 2, locale) <= 0 ||
+			pg_strncoll("xyz", 3, "abc", 2, locale) <= 0)
+			elog(ERROR, "C-locale comparison result is wrong");
+
+		if (!pg_strxfrm_enabled(locale))
+			elog(ERROR, "pg_strxfrm_enabled() is false for C locale");
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() size probe returned %zu, expected 3", n);
+		n = pg_strnxfrm(buf, 4, "abc", 3, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strnxfrm() produced \"%s\"", buf);
+		n = pg_strxfrm(buf, "abc", 4, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strxfrm() produced \"%s\"", buf);
+		n = pg_strnxfrm(buf, 3, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() destsize==srclen returned %zu", n);
+		n = pg_strnxfrm(buf, 2, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() short dest returned %zu", n);
+
+		if (!pg_strxfrm_prefix_enabled(locale))
+			elog(ERROR, "pg_strxfrm_prefix_enabled() is false for C locale");
+		n = pg_strnxfrm_prefix(NULL, 0, "abcdef", 6, locale);
+		if (n != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize 0 returned %zu", n);
+		n = pg_strnxfrm_prefix(pfx, 2, "abcdef", 6, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() produced a wrong prefix");
+		n = pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale);
+		if (n != 3 || memcmp(pfx, "abc", 3) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize>=srclen produced a wrong result");
+		n = pg_strxfrm_prefix(pfx, "abcdef", 2, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strxfrm_prefix() produced a wrong prefix");
+
+		if (pg_strxfrm(x1, "abc", sizeof(x1), locale) >= sizeof(x1) ||
+			pg_strxfrm(x2, "abd", sizeof(x2), locale) >= sizeof(x2) ||
+			(strcmp(x1, x2) < 0) != (pg_strcoll("abc", "abd", locale) < 0))
+			elog(ERROR, "pg_strxfrm() disagrees with pg_strcoll()");
+	}
+	else
+	{
+		char	   *tmp;
+
+		if (locale->collate == NULL)
+			elog(ERROR, "collate methods missing for non-C locale");
+
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		tmp = palloc(n + 1);
+		if (pg_strnxfrm(tmp, n + 1, "abc", 3, locale) > n)
+			elog(ERROR, "pg_strnxfrm() grew on the second call");
+		pfree(tmp);
+
+		if (pg_strxfrm_prefix_enabled(locale) &&
+			pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale) > sizeof(pfx))
+			elog(ERROR, "pg_strnxfrm_prefix() exceeded destsize");
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_pg_locale_apis);
+
+Datum
+test_pg_locale_apis(PG_FUNCTION_ARGS)
+{
+	pg_locale_t locale;
+
+	locale = pg_newlocale_from_collation(PG_GETARG_OID(0));
+	if (locale == NULL)
+		elog(ERROR, "pg_newlocale_from_collation() returned NULL");
+
+	test_collate(locale);
+	test_case_mapping(locale);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.control b/src/test/modules/test_pg_locale/test_pg_locale.control
new file mode 100644
index 00000000000..6b224d04a1b
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.control
@@ -0,0 +1,4 @@
+comment = 'Test code for pg_locale.h APIs'
+default_version = '1.0'
+module_pathname = '$libdir/test_pg_locale'
+relocatable = true
-- 
2.43.0

From 497a4685ddbcf2880a076604721a3e2b53453f5e 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/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 ba30a89ef3c424b4436c91373001668daef068bf 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/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 4b201e6dd10d59ea5b8d636f03b73e4d5ff4fdc2 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Sat, 15 Aug 2026 12:26:09 -0700
Subject: [PATCH vPG20 3/3] Add C test module for pg_locale.h APIs.

Test the API independently to account for fallback paths that aren't
adequately tested from SQL.

The backport to 18 also tests the previously-supported behavior where
a size of -1 meant that the string was NUL-terminated. That behavior
was later removed in 19.

Discussion: https://postgr.es/m/v36ssaygf7grb3qzfsjhtdzi7kqd45ds56nyuf7gi5qjml4qbb@ezmfqzmhlrs2
Backpatch-through: 18
---
 src/test/modules/Makefile                     |   1 +
 src/test/modules/meson.build                  |   1 +
 src/test/modules/test_pg_locale/.gitignore    |   4 +
 src/test/modules/test_pg_locale/Makefile      |  23 +++
 src/test/modules/test_pg_locale/README        |   2 +
 .../expected/test_pg_locale.out               |  38 +++++
 src/test/modules/test_pg_locale/meson.build   |  33 ++++
 .../test_pg_locale/sql/test_pg_locale.sql     |  23 +++
 .../test_pg_locale/test_pg_locale--1.0.sql    |   8 +
 .../modules/test_pg_locale/test_pg_locale.c   | 153 ++++++++++++++++++
 .../test_pg_locale/test_pg_locale.control     |   4 +
 11 files changed, 290 insertions(+)
 create mode 100644 src/test/modules/test_pg_locale/.gitignore
 create mode 100644 src/test/modules/test_pg_locale/Makefile
 create mode 100644 src/test/modules/test_pg_locale/README
 create mode 100644 src/test/modules/test_pg_locale/expected/test_pg_locale.out
 create mode 100644 src/test/modules/test_pg_locale/meson.build
 create mode 100644 src/test/modules/test_pg_locale/sql/test_pg_locale.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.c
 create mode 100644 src/test/modules/test_pg_locale/test_pg_locale.control

diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 098bb8142ae..8a2b09bd11e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -41,6 +41,7 @@ SUBDIRS = \
 		  test_oat_hooks \
 		  test_parser \
 		  test_pg_dump \
+		  test_pg_locale \
 		  test_plan_advice \
 		  test_predtest \
 		  test_radixtree \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 4bca42bb370..71c4035b1b3 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -42,6 +42,7 @@ subdir('test_misc')
 subdir('test_oat_hooks')
 subdir('test_parser')
 subdir('test_pg_dump')
+subdir('test_pg_locale')
 subdir('test_plan_advice')
 subdir('test_predtest')
 subdir('test_radixtree')
diff --git a/src/test/modules/test_pg_locale/.gitignore b/src/test/modules/test_pg_locale/.gitignore
new file mode 100644
index 00000000000..5dcb3ff9723
--- /dev/null
+++ b/src/test/modules/test_pg_locale/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/src/test/modules/test_pg_locale/Makefile b/src/test/modules/test_pg_locale/Makefile
new file mode 100644
index 00000000000..9b051f8a697
--- /dev/null
+++ b/src/test/modules/test_pg_locale/Makefile
@@ -0,0 +1,23 @@
+# src/test/modules/test_pg_locale/Makefile
+
+MODULE_big = test_pg_locale
+OBJS = \
+	$(WIN32RES) \
+	test_pg_locale.o
+PGFILEDESC = "test_pg_locale - test code for pg_locale.h APIs"
+
+EXTENSION = test_pg_locale
+DATA = test_pg_locale--1.0.sql
+
+REGRESS = test_pg_locale
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_pg_locale
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_pg_locale/README b/src/test/modules/test_pg_locale/README
new file mode 100644
index 00000000000..d95af97c005
--- /dev/null
+++ b/src/test/modules/test_pg_locale/README
@@ -0,0 +1,2 @@
+Calls pg_locale.h wrappers directly.  Ordinary SQL tests do not reach
+the C-locale fallbacks because in-tree callers special-case collate_is_c.
diff --git a/src/test/modules/test_pg_locale/expected/test_pg_locale.out b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
new file mode 100644
index 00000000000..edbba284552
--- /dev/null
+++ b/src/test/modules/test_pg_locale/expected/test_pg_locale.out
@@ -0,0 +1,38 @@
+CREATE EXTENSION test_pg_locale;
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
+ test_pg_locale_apis 
+---------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_pg_locale/meson.build b/src/test/modules/test_pg_locale/meson.build
new file mode 100644
index 00000000000..f5097464ad2
--- /dev/null
+++ b/src/test/modules/test_pg_locale/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_pg_locale_sources = files(
+  'test_pg_locale.c',
+)
+
+if host_system == 'windows'
+  test_pg_locale_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_pg_locale',
+    '--FILEDESC', 'test_pg_locale - test code for pg_locale.h APIs',])
+endif
+
+test_pg_locale = shared_module('test_pg_locale',
+  test_pg_locale_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_pg_locale
+
+test_install_data += files(
+  'test_pg_locale.control',
+  'test_pg_locale--1.0.sql',
+)
+
+tests += {
+  'name': 'test_pg_locale',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'test_pg_locale',
+    ],
+  },
+}
diff --git a/src/test/modules/test_pg_locale/sql/test_pg_locale.sql b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
new file mode 100644
index 00000000000..212012feb9c
--- /dev/null
+++ b/src/test/modules/test_pg_locale/sql/test_pg_locale.sql
@@ -0,0 +1,23 @@
+CREATE EXTENSION test_pg_locale;
+
+--
+-- These tests don't produce any interesting output.  We're checking that
+-- the operations complete without crashing and that none of their internal
+-- sanity tests fail.
+--
+-- to_regcollation() returns NULL when the collation is absent or is not
+-- usable in the current database encoding.  The function is STRICT, so
+-- those cases are skipped.
+--
+
+-- Libc C.  Available in every database.
+SELECT test_pg_locale_apis(to_regcollation('"C"'));
+
+-- Builtin C (collate and ctype).  Usable only in UTF8 databases.
+SELECT test_pg_locale_apis(to_regcollation('ucs_basic'));
+
+-- Builtin C.UTF-8 (C collate, Unicode ctype).  Same encoding restriction.
+SELECT test_pg_locale_apis(to_regcollation('pg_c_utf8'));
+
+-- en-x-icu is present when ICU collations were imported at initdb.
+SELECT test_pg_locale_apis(to_regcollation('en-x-icu'));
diff --git a/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
new file mode 100644
index 00000000000..134c4befa06
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale--1.0.sql
@@ -0,0 +1,8 @@
+/* src/test/modules/test_pg_locale/test_pg_locale--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_pg_locale" to load this file. \quit
+
+CREATE FUNCTION test_pg_locale_apis(oid)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.c b/src/test/modules/test_pg_locale/test_pg_locale.c
new file mode 100644
index 00000000000..122152f5fcb
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.c
@@ -0,0 +1,153 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_pg_locale.c
+ *		Call pg_locale.h wrappers directly.
+ *
+ * SQL callers special-case collate_is_c, so the C-locale fallbacks are
+ * not reached by ordinary regression tests.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_pg_locale/test_pg_locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/pg_locale.h"
+
+PG_MODULE_MAGIC;
+
+static void
+test_case_mapping(pg_locale_t locale)
+{
+	char		buf[32];
+	size_t		n;
+
+	n = pg_strlower(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strlower() size probe returned %zu, expected 3", n);
+	n = pg_strlower(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strlower() produced \"%s\"", buf);
+
+	n = pg_strupper(NULL, 0, "AbC", 3, locale);
+	if (n != 3)
+		elog(ERROR, "pg_strupper() size probe returned %zu, expected 3", n);
+	n = pg_strupper(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "ABC") != 0)
+		elog(ERROR, "pg_strupper() produced \"%s\"", buf);
+
+	n = pg_strfold(buf, 4, "AbC", 3, locale);
+	if (n != 3 || strcmp(buf, "abc") != 0)
+		elog(ERROR, "pg_strfold() produced \"%s\"", buf);
+
+	buf[0] = '\0';
+	n = pg_strtitle(buf, sizeof(buf), "hello-world", 11, locale);
+	if (n != 11)
+		elog(ERROR, "pg_strtitle() returned %zu, expected 11", n);
+	if (locale->ctype_is_c && strcmp(buf, "Hello-World") != 0)
+		elog(ERROR, "pg_strtitle() produced \"%s\"", buf);
+}
+
+static void
+test_collate(pg_locale_t locale)
+{
+	char		buf[32];
+	char		pfx[8];
+	char		x1[8];
+	char		x2[8];
+	size_t		n;
+
+	if (pg_strcoll("abc", "abc", locale) != 0 ||
+		pg_strncoll("abc", 3, "abc", 3, locale) != 0 ||
+		pg_strcoll("", "", locale) != 0)
+		elog(ERROR, "equal strings did not compare equal");
+
+	if (locale->collate_is_c)
+	{
+		if (locale->collate != NULL)
+			elog(ERROR, "collate_is_c but collate methods are set");
+		if (pg_strcoll("abc", "abd", locale) >= 0 ||
+			pg_strcoll("abd", "abc", locale) <= 0 ||
+			pg_strncoll("ab", 2, "abc", 3, locale) >= 0 ||
+			pg_strncoll("abc", 3, "ab", 2, locale) <= 0 ||
+			pg_strncoll("xyz", 3, "abc", 2, locale) <= 0)
+			elog(ERROR, "C-locale comparison result is wrong");
+
+		if (!pg_strxfrm_enabled(locale))
+			elog(ERROR, "pg_strxfrm_enabled() is false for C locale");
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() size probe returned %zu, expected 3", n);
+		n = pg_strnxfrm(buf, 4, "abc", 3, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strnxfrm() produced \"%s\"", buf);
+		n = pg_strxfrm(buf, "abc", 4, locale);
+		if (n != 3 || strcmp(buf, "abc") != 0)
+			elog(ERROR, "pg_strxfrm() produced \"%s\"", buf);
+		n = pg_strnxfrm(buf, 3, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() destsize==srclen returned %zu", n);
+		n = pg_strnxfrm(buf, 2, "abc", 3, locale);
+		if (n != 3)
+			elog(ERROR, "pg_strnxfrm() short dest returned %zu", n);
+
+		if (!pg_strxfrm_prefix_enabled(locale))
+			elog(ERROR, "pg_strxfrm_prefix_enabled() is false for C locale");
+		n = pg_strnxfrm_prefix(NULL, 0, "abcdef", 6, locale);
+		if (n != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize 0 returned %zu", n);
+		n = pg_strnxfrm_prefix(pfx, 2, "abcdef", 6, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() produced a wrong prefix");
+		n = pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale);
+		if (n != 3 || memcmp(pfx, "abc", 3) != 0)
+			elog(ERROR, "pg_strnxfrm_prefix() destsize>=srclen produced a wrong result");
+		n = pg_strxfrm_prefix(pfx, "abcdef", 2, locale);
+		if (n != 2 || memcmp(pfx, "ab", 2) != 0)
+			elog(ERROR, "pg_strxfrm_prefix() produced a wrong prefix");
+
+		if (pg_strxfrm(x1, "abc", sizeof(x1), locale) >= sizeof(x1) ||
+			pg_strxfrm(x2, "abd", sizeof(x2), locale) >= sizeof(x2) ||
+			(strcmp(x1, x2) < 0) != (pg_strcoll("abc", "abd", locale) < 0))
+			elog(ERROR, "pg_strxfrm() disagrees with pg_strcoll()");
+	}
+	else
+	{
+		char	   *tmp;
+
+		if (locale->collate == NULL)
+			elog(ERROR, "collate methods missing for non-C locale");
+
+		n = pg_strnxfrm(NULL, 0, "abc", 3, locale);
+		tmp = palloc(n + 1);
+		if (pg_strnxfrm(tmp, n + 1, "abc", 3, locale) > n)
+			elog(ERROR, "pg_strnxfrm() grew on the second call");
+		pfree(tmp);
+
+		if (pg_strxfrm_prefix_enabled(locale) &&
+			pg_strnxfrm_prefix(pfx, sizeof(pfx), "abc", 3, locale) > sizeof(pfx))
+			elog(ERROR, "pg_strnxfrm_prefix() exceeded destsize");
+	}
+}
+
+PG_FUNCTION_INFO_V1(test_pg_locale_apis);
+
+Datum
+test_pg_locale_apis(PG_FUNCTION_ARGS)
+{
+	pg_locale_t locale;
+
+	locale = pg_newlocale_from_collation(PG_GETARG_OID(0));
+	if (locale == NULL)
+		elog(ERROR, "pg_newlocale_from_collation() returned NULL");
+
+	test_collate(locale);
+	test_case_mapping(locale);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/test/modules/test_pg_locale/test_pg_locale.control b/src/test/modules/test_pg_locale/test_pg_locale.control
new file mode 100644
index 00000000000..6b224d04a1b
--- /dev/null
+++ b/src/test/modules/test_pg_locale/test_pg_locale.control
@@ -0,0 +1,4 @@
+comment = 'Test code for pg_locale.h APIs'
+default_version = '1.0'
+module_pathname = '$libdir/test_pg_locale'
+relocatable = true
-- 
2.43.0

Reply via email to