From ccbcc7325d6096741b48f536c0eceea73e858ac1 Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Tue, 25 Aug 2026 22:43:10 +0800
Subject: [PATCH v1] Fix right() with the most negative integer

A negative n means "return all but the first |n| characters", so
text_right() negates n before clipping.  Negating PG_INT32_MIN overflows;
with -fwrapv the result is PG_INT32_MIN again, still negative, and
pg_mbcharcliplen() then returns an offset of zero, so the whole string is
returned where the correct answer is an empty string:

    SELECT right('abcdef', (-2147483648)::int4);   -- 'abcdef', want ''
    SELECT right('abcdef', -2147483647);           -- '', correct

Clamp to PG_INT32_MAX instead.  Any n whose absolute value is at least the
string's length skips all of it, and a text value cannot be longer than
PG_INT32_MAX, so this gives the same answer for every other input.  Note
that erroring out, as text_format_string_conversion() does for a width of
INT_MIN a few hundred lines away, would not be right here: unlike a format
width, an out-of-range skip count has a well-defined result.

text_left() is not affected.  Its negative case computes the character
length plus n rather than negating n, and since the length is non-negative
and bounded by the varlena size limit that sum cannot overflow.
---
 src/backend/utils/adt/varlena.c    | 12 +++++++++++-
 src/test/regress/expected/text.out |  8 ++++++++
 src/test/regress/sql/text.sql      |  3 +++
 3 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index a09a9e5d5bb..3117069cf1a 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -4714,7 +4714,17 @@ text_right(PG_FUNCTION_ARGS)
 	int			off;
 
 	if (n < 0)
-		n = -n;
+	{
+		/*
+		 * Negating PG_INT32_MIN would overflow, so clamp instead.  Any n whose
+		 * absolute value is at least the string's length skips the whole
+		 * string, and len can't exceed PG_INT32_MAX, so this is equivalent.
+		 */
+		if (unlikely(n == PG_INT32_MIN))
+			n = PG_INT32_MAX;
+		else
+			n = -n;
+	}
 	else
 		n = pg_mbstrlen_with_len(p, len) - n;
 	off = pg_mbcharcliplen(p, len, n);
diff --git a/src/test/regress/expected/text.out b/src/test/regress/expected/text.out
index 3f9982388ba..9ef23f4ddea 100644
--- a/src/test/regress/expected/text.out
+++ b/src/test/regress/expected/text.out
@@ -118,6 +118,14 @@ select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) ord
   5 | ahoj | ahoj
 (11 rows)
 
+-- the most negative value must skip the whole string, same as any other n
+-- whose absolute value exceeds its length
+select left('ahoj', (-2147483648)::int4), right('ahoj', (-2147483648)::int4);
+ left | right 
+------+-------
+      | 
+(1 row)
+
 select quote_literal('');
  quote_literal 
 ---------------
diff --git a/src/test/regress/sql/text.sql b/src/test/regress/sql/text.sql
index 540e551254d..3a9be863bb7 100644
--- a/src/test/regress/sql/text.sql
+++ b/src/test/regress/sql/text.sql
@@ -37,6 +37,9 @@ select concat_ws('',10,20,null,30);
 select concat_ws(NULL,10,20,null,30) is null;
 select reverse('abcde');
 select i, left('ahoj', i), right('ahoj', i) from generate_series(-5, 5) t(i) order by i;
+-- the most negative value must skip the whole string, same as any other n
+-- whose absolute value exceeds its length
+select left('ahoj', (-2147483648)::int4), right('ahoj', (-2147483648)::int4);
 select quote_literal('');
 select quote_literal('abc''');
 select quote_literal(e'\\');
-- 
2.47.3

