From 8bdb4237a3b08b192ddfa7d69128bff3e0b92d47 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Wed, 23 Sep 2026 11:47:23 -0500 Subject: [PATCH v2 2/2] Optimize padding in lpad() and rpad(). lpad() and rpad() write the padding one character at a time, calling pg_mblen_range() and memcpy() for each one, which gets slow once the padding runs to many kilobytes. Since the padding is just the pad string repeated, this commit copies the pad string once, as before, and then produces the rest by copying what has already been written onto the end of itself, doubling the length each time. That takes a handful of memcpy() calls for a pad string of any length, and it is dramatically faster for long padding. Note that the pad string is still only checked for a truncated multibyte character as far as it is actually used, so a bad tail is accepted when no padding is needed, as before. Co-authored-by: Sehrope Sarkuni Discussion: https://postgr.es/m/CAH7T-apj%2BpFg9bRkXVGfGejS2Uu4WzdMd7KoLKTW11mdsrt1Ew%40mail.gmail.com --- src/backend/utils/adt/oracle_compat.c | 45 ++++++++++++++++++++++++-- src/test/regress/expected/encoding.out | 19 +++++++++++ src/test/regress/sql/encoding.sql | 7 ++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index 2adc3874bc2..d54fe948355 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -147,22 +147,61 @@ casefold(PG_FUNCTION_ARGS) * Append m characters of the padding string s2 (s2len bytes) to dst, * cycling through s2 as needed, and return a pointer past the last byte * written. + * + * Note that s2 is only checked for a truncated multibyte character as far as + * we actually use it, so a bad tail is accepted when the padding stops short + * of it, as it always has been. */ static char * append_padding(char *dst, const char *s2, int s2len, int m) { const char *ptr2 = s2; const char *ptr2end = s2 + s2len; + char *start = dst; + int nchars = 0; + int nbytes; + int written; - while (m--) + /* Copy s2 once, one character at a time, or until m runs out. */ + while (m > 0 && ptr2 < ptr2end) { int mlen = pg_mblen_range(ptr2, ptr2end); memcpy(dst, ptr2, mlen); dst += mlen; ptr2 += mlen; - if (ptr2 == ptr2end) /* wrap around at end of s2 */ - ptr2 = s2; + m--; + nchars++; + } + + if (m == 0) + return dst; + + /* + * The rest of the padding is s2 repeated, so work out how many bytes that + * is: whole copies of s2, plus the first m % nchars characters of one + * more. + */ + nbytes = (m / nchars) * s2len; + ptr2 = s2; + for (m %= nchars; m > 0; m--) + ptr2 += pg_mblen_unbounded(ptr2); + nbytes += ptr2 - s2; + + /* + * Now produce those bytes by copying what we've already written, doubling + * the length each time, so that the work is done by a few large memcpy() + * calls rather than one per character. + */ + written = dst - start; + while (nbytes > 0) + { + int chunk = Min(written, nbytes); + + memcpy(dst, start, chunk); + dst += chunk; + nbytes -= chunk; + written += chunk; } return dst; diff --git a/src/test/regress/expected/encoding.out b/src/test/regress/expected/encoding.out index 0bb72a1df6f..8098599e302 100644 --- a/src/test/regress/expected/encoding.out +++ b/src/test/regress/expected/encoding.out @@ -60,6 +60,25 @@ SELECT reverse(good) FROM regress_encoding; éfac (1 row) +-- padding with a multibyte character +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; + lpad | rpad +---------+--------- + ééécafé | caféééé +(1 row) + +-- padding with a lone lead byte of a multibyte character = error +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT rpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +-- no error when no padding is needed +SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding; + lpad | rpad +------+------ + café | café +(1 row) + -- invalid short mb character = error SELECT length(truncated) FROM regress_encoding; ERROR: invalid byte sequence for encoding "UTF8": 0xc3 diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql index 26caa93a5d5..7121704f790 100644 --- a/src/test/regress/sql/encoding.sql +++ b/src/test/regress/sql/encoding.sql @@ -37,6 +37,13 @@ SELECT substring(good, 3, 1) FROM regress_encoding; SELECT substring(good, 4, 1) FROM regress_encoding; SELECT regexp_replace(good, '^caf(.)$', '\1') FROM regress_encoding; SELECT reverse(good) FROM regress_encoding; +-- padding with a multibyte character +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; +-- padding with a lone lead byte of a multibyte character = error +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT rpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +-- no error when no padding is needed +SELECT lpad(good, 4, test_bytea_to_text('\xc3')), rpad(good, 4, test_bytea_to_text('\xc3')) FROM regress_encoding; -- invalid short mb character = error SELECT length(truncated) FROM regress_encoding; -- 2.55.0