On Mon, 2026-06-29 at 18:29 -0700, Noah Misch wrote:
> This became refactor commit 5a38104.  In a UTF8 database, the commit
> changed
> the behavior of:
> 
>   SELECT 'café' ILIKE 'caf_' COLLATE "C";
> 
> That query reports true in v18, false in v19.

Thank you for the report! Fixed in 0001, with test.

>   I've copied Claude's whole
> review for the commit to
> https://docs.google.com/document/d/19_xKpKUQlY5N51qbfXyipXs3rqIYEC3qk5oaUDJ48YA/edit?tab=t.0
> .
> Two of its other findings sound potentially important, but I didn't
> verify
> them.

I'll refer to the issue numbers below (NB: the doc's summary uses
different numbering, which is confusing):

Issue 1: Real issue for windows and code points > 0xFFFF. Other cases
are just defensive, I believe. Fixed in 0003.

Issue 2: I don't see an issue here, though sort of related to the fix
in 0003.

Issue 3: I don't see an issue here.

Issue 4: The issue you raised above; fixed in 0001.

Issue 5: I don't see an issue here.

I also addressed summary item 4, the comment block fix, in 0002. And I
added some missing casts in 0004.

Does that cover the two other findings you were concerned about?


In the future, if practical, can you please include reports like this
as text attachments for the sake of the archives?

Regards,
        Jeff Davis

From bcbe4b1bbce253279e0d55aef2f6160936c7409a Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 6 Jul 2026 10:27:57 -0700
Subject: [PATCH v1 1/4] Fix unintentional behavior change from 5a38104b36.

Reported-by: Noah Misch <[email protected]>
Discussion: https://postgr.es/m/[email protected]
Backpatch-through: 19
---
 src/backend/utils/adt/like.c               | 6 ++++--
 src/test/regress/expected/collate.utf8.out | 7 +++++++
 src/test/regress/sql/collate.utf8.sql      | 3 +++
 3 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/adt/like.c b/src/backend/utils/adt/like.c
index 350bc07f210..64147bc5b5c 100644
--- a/src/backend/utils/adt/like.c
+++ b/src/backend/utils/adt/like.c
@@ -191,12 +191,14 @@ Generic_Text_IC_like(text *str, text *pat, Oid collation)
 
 	/*
 	 * For efficiency reasons, in the C locale we don't call lower() on the
-	 * pattern and text, but instead lowercase each character lazily.
+	 * pattern and text, but instead lowercase each character lazily. This
+	 * only works for single-byte encodings, otherwise "_" may incorrectly
+	 * match an incomplete byte sequence.
 	 *
 	 * XXX: use casefolding instead?
 	 */
 
-	if (locale->ctype_is_c)
+	if (locale->ctype_is_c && pg_database_encoding_max_length() == 1)
 	{
 		p = VARDATA_ANY(pat);
 		plen = VARSIZE_ANY_EXHDR(pat);
diff --git a/src/test/regress/expected/collate.utf8.out b/src/test/regress/expected/collate.utf8.out
index 0c3ab5c89b2..72606180e99 100644
--- a/src/test/regress/expected/collate.utf8.out
+++ b/src/test/regress/expected/collate.utf8.out
@@ -34,6 +34,13 @@ SELECT U&'\00C1\00E1' !~ '[[:alpha:]]' COLLATE regress_builtin_c;
 (1 row)
 
 DROP COLLATION regress_builtin_c;
+-- a '_' matches a multibyte character
+SELECT 'café' ILIKE 'caf_' COLLATE "C";
+ ?column? 
+----------
+ t
+(1 row)
+
 --
 -- Test PG_C_UTF8
 --
diff --git a/src/test/regress/sql/collate.utf8.sql b/src/test/regress/sql/collate.utf8.sql
index d6d14220ab3..ad05f7ac15f 100644
--- a/src/test/regress/sql/collate.utf8.sql
+++ b/src/test/regress/sql/collate.utf8.sql
@@ -26,6 +26,9 @@ SELECT U&'\00C1\00E1' !~ '[[:alpha:]]' COLLATE regress_builtin_c;
 
 DROP COLLATION regress_builtin_c;
 
+-- a '_' matches a multibyte character
+SELECT 'café' ILIKE 'caf_' COLLATE "C";
+
 --
 -- Test PG_C_UTF8
 --
-- 
2.43.0

From 25780d1ebd118e5cc2ba653293d6da1ad28d1108 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 6 Jul 2026 11:02:56 -0700
Subject: [PATCH v1 2/4] Fix obsolete comment.

Reported-by: Noah Misch <[email protected]>
Discussion: https://postgr.es/m/[email protected]
Backpatch-through: 19
---
 src/backend/utils/adt/pg_locale_libc.c | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c
index 006039534a9..37e26b7e7a8 100644
--- a/src/backend/utils/adt/pg_locale_libc.c
+++ b/src/backend/utils/adt/pg_locale_libc.c
@@ -64,11 +64,6 @@
  * where this matters is treatment of I/i in Turkish, and the behavior is
  * meant to match the upper()/lower() SQL functions.
  *
- * We store the active collation setting in static variables.  In principle
- * it could be passed down to here via the regex library's "struct vars" data
- * structure; but that would require somewhat invasive changes in the regex
- * library, and right now there's no real benefit to be gained from that.
- *
  * NB: the coding here assumes pg_wchar is an unsigned type.
  */
 
-- 
2.43.0

From e1c2be96183d5d2e29785a92d14fb9a06063b85b Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 6 Jul 2026 11:17:10 -0700
Subject: [PATCH v1 3/4] pg_locale_libc.c: add guards to ctype methods.

Necessary for 16-bit wchar_t platforms (Windows).

Other guards are just defensive. Also correct style issue with
branches.

Reported-by: Noah Misch <[email protected]>
Discussion: https://postgr.es/m/[email protected]
Backpatch-through: 19
---
 src/backend/utils/adt/pg_locale_libc.c | 52 ++++++++++++++++++++++++--
 1 file changed, 48 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c
index 37e26b7e7a8..0043371586d 100644
--- a/src/backend/utils/adt/pg_locale_libc.c
+++ b/src/backend/utils/adt/pg_locale_libc.c
@@ -124,60 +124,80 @@ static size_t strupper_libc_mb(char *dest, size_t destsize,
 static bool
 wc_isdigit_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isdigit_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isalpha_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isalpha_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isalnum_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isalnum_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isupper_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isupper_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_islower_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return islower_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isgraph_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isgraph_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isprint_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isprint_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_ispunct_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return ispunct_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isspace_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isspace_l((unsigned char) wc, locale->lt);
 }
 
 static bool
 wc_isxdigit_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 #ifndef WIN32
 	return isxdigit_l((unsigned char) wc, locale->lt);
 #else
@@ -188,6 +208,8 @@ wc_isxdigit_libc_sb(pg_wchar wc, pg_locale_t locale)
 static bool
 wc_iscased_libc_sb(pg_wchar wc, pg_locale_t locale)
 {
+	if (wc > UCHAR_MAX)
+		return false;
 	return isupper_l((unsigned char) wc, locale->lt) ||
 		islower_l((unsigned char) wc, locale->lt);
 }
@@ -195,60 +217,80 @@ wc_iscased_libc_sb(pg_wchar wc, pg_locale_t locale)
 static bool
 wc_isdigit_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswdigit_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isalpha_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswalpha_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isalnum_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswalnum_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isupper_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswupper_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_islower_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswlower_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isgraph_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswgraph_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isprint_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswprint_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_ispunct_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswpunct_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isspace_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswspace_l((wint_t) wc, locale->lt);
 }
 
 static bool
 wc_isxdigit_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 #ifndef WIN32
 	return iswxdigit_l((wint_t) wc, locale->lt);
 #else
@@ -259,6 +301,8 @@ wc_isxdigit_libc_mb(pg_wchar wc, pg_locale_t locale)
 static bool
 wc_iscased_libc_mb(pg_wchar wc, pg_locale_t locale)
 {
+	if (sizeof(wchar_t) < 4 && wc > (pg_wchar) 0xFFFF)
+		return false;
 	return iswupper_l((wint_t) wc, locale->lt) ||
 		iswlower_l((wint_t) wc, locale->lt);
 }
@@ -271,7 +315,7 @@ toupper_libc_sb(pg_wchar wc, pg_locale_t locale)
 	/* force C behavior for ASCII characters, per comments above */
 	if (locale->is_default && wc <= (pg_wchar) 127)
 		return pg_ascii_toupper((unsigned char) wc);
-	if (wc <= (pg_wchar) UCHAR_MAX)
+	else if (wc <= (pg_wchar) UCHAR_MAX)
 		return toupper_l((unsigned char) wc, locale->lt);
 	else
 		return wc;
@@ -285,7 +329,7 @@ toupper_libc_mb(pg_wchar wc, pg_locale_t locale)
 	/* force C behavior for ASCII characters, per comments above */
 	if (locale->is_default && wc <= (pg_wchar) 127)
 		return pg_ascii_toupper((unsigned char) wc);
-	if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF)
+	else if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF)
 		return towupper_l((wint_t) wc, locale->lt);
 	else
 		return wc;
@@ -299,7 +343,7 @@ tolower_libc_sb(pg_wchar wc, pg_locale_t locale)
 	/* force C behavior for ASCII characters, per comments above */
 	if (locale->is_default && wc <= (pg_wchar) 127)
 		return pg_ascii_tolower((unsigned char) wc);
-	if (wc <= (pg_wchar) UCHAR_MAX)
+	else if (wc <= (pg_wchar) UCHAR_MAX)
 		return tolower_l((unsigned char) wc, locale->lt);
 	else
 		return wc;
@@ -313,7 +357,7 @@ tolower_libc_mb(pg_wchar wc, pg_locale_t locale)
 	/* force C behavior for ASCII characters, per comments above */
 	if (locale->is_default && wc <= (pg_wchar) 127)
 		return pg_ascii_tolower((unsigned char) wc);
-	if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF)
+	else if (sizeof(wchar_t) >= 4 || wc <= (pg_wchar) 0xFFFF)
 		return towlower_l((wint_t) wc, locale->lt);
 	else
 		return wc;
-- 
2.43.0

From 132781a2fbf1454c631b4cd116725f075895a7cb Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 6 Jul 2026 12:02:54 -0700
Subject: [PATCH v1 4/4] pg_locale_libc.c: add missing casts to unsigned char.

Discussion: https://postgr.es/m/[email protected]
Backpatch-through: 19
---
 src/backend/utils/adt/pg_locale_libc.c | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c
index 0043371586d..b50b3f24efd 100644
--- a/src/backend/utils/adt/pg_locale_libc.c
+++ b/src/backend/utils/adt/pg_locale_libc.c
@@ -527,7 +527,7 @@ strlower_libc_sb(char *dest, size_t destsize, const char *src, size_t srclen,
 			{
 				if (*p >= 'A' && *p <= 'Z')
 					*p += 'a' - 'A';
-				else if (IS_HIGHBIT_SET(*p) && isupper_l(*p, loc))
+				else if (IS_HIGHBIT_SET(*p) && isupper_l((unsigned char) *p, loc))
 					*p = tolower_l((unsigned char) *p, loc);
 			}
 			else
@@ -611,14 +611,14 @@ strtitle_libc_sb(char *dest, size_t destsize, const char *src, size_t srclen,
 				{
 					if (*p >= 'A' && *p <= 'Z')
 						*p += 'a' - 'A';
-					else if (IS_HIGHBIT_SET(*p) && isupper_l(*p, loc))
+					else if (IS_HIGHBIT_SET(*p) && isupper_l((unsigned char) *p, loc))
 						*p = tolower_l((unsigned char) *p, loc);
 				}
 				else
 				{
 					if (*p >= 'a' && *p <= 'z')
 						*p -= 'a' - 'A';
-					else if (IS_HIGHBIT_SET(*p) && islower_l(*p, loc))
+					else if (IS_HIGHBIT_SET(*p) && islower_l((unsigned char) *p, loc))
 						*p = toupper_l((unsigned char) *p, loc);
 				}
 			}
@@ -713,7 +713,7 @@ strupper_libc_sb(char *dest, size_t destsize, const char *src, size_t srclen,
 			{
 				if (*p >= 'a' && *p <= 'z')
 					*p -= 'a' - 'A';
-				else if (IS_HIGHBIT_SET(*p) && islower_l(*p, loc))
+				else if (IS_HIGHBIT_SET(*p) && islower_l((unsigned char) *p, loc))
 					*p = toupper_l((unsigned char) *p, loc);
 			}
 			else
-- 
2.43.0

Reply via email to