From 4b201e6dd10d59ea5b8d636f03b73e4d5ff4fdc2 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
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

