From a5007ff32afe3bd608a88d0f64a79bdb327e8448 Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Wed, 23 Sep 2026 21:41:57 +0000 Subject: [PATCH v4 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. --- src/backend/utils/adt/oracle_compat.c | 47 ++++++++++++++++++++++++-- src/test/regress/expected/encoding.out | 37 ++++++++++++++++++++ src/test/regress/expected/strings.out | 26 ++++++++++++++ src/test/regress/sql/encoding.sql | 10 ++++++ src/test/regress/sql/strings.sql | 7 ++++ 5 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index e5238e44813..f457e01a2ae 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -147,22 +147,63 @@ casefold(PG_FUNCTION_ARGS) * Append m characters of the padding string pad (padlen bytes) to dst, * cycling through pad as needed, and return a pointer past the last byte * written. + * + * The pad string is validated with pg_mblen_range() only as far as it is + * used, so an incomplete multibyte character at its end is an error only + * if the padding reaches it. */ static char * append_padding(char *dst, const char *pad, int padlen, int m) { const char *p = pad; const char *pend = pad + padlen; + char *start = dst; + int nchars = 0; + int nbytes; + int written; - while (m--) + /* copy pad once, one character at a time, or until m runs out */ + while (m > 0 && p < pend) { int mlen = pg_mblen_range(p, pend); memcpy(dst, p, mlen); dst += mlen; p += mlen; - if (p == pend) /* wrap around at end of pad */ - p = pad; + m--; + nchars++; + } + + if (m == 0) + return dst; + + /* + * The rest of the padding is pad repeated, so work out how many bytes + * that is: whole copies of pad, plus the first m % nchars characters of + * one more. + */ + nbytes = (m / nchars) * padlen; + p = pad; + for (m %= nchars; m > 0; m--) + p += pg_mblen_unbounded(p); + nbytes += p - pad; + + /* + * Produce those bytes by copying what has already been written onto the + * end, doubling the length each time, so the work is done by a few large + * memcpy() calls rather than one per character. The last chunk, if + * shorter, is a prefix of the padding written so far and therefore of + * pad, which is the partial final repetition. + */ + 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..9fc871215b0 100644 --- a/src/test/regress/expected/encoding.out +++ b/src/test/regress/expected/encoding.out @@ -60,6 +60,43 @@ SELECT reverse(good) FROM regress_encoding; éfac (1 row) +-- multibyte pad strings: whole repetitions, a partial final repetition, and +-- fewer than one repetition +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; + lpad | rpad +---------+--------- + ééécafé | caféééé +(1 row) + +SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding; + lpad | rpad +--------------+-------------- + éabéabéacafé | cafééabéabéa +(1 row) + +SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding; + lpad | rpad +-------+------- + écafé | caféé +(1 row) + +-- a lone lead byte in the pad string is an error if the padding reaches it +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +ERROR: invalid byte sequence for encoding "UTF8": 0xc3 +SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; + lpad | rpad +-------+-------- + acafé | caféab +(1 row) + +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/expected/strings.out b/src/test/regress/expected/strings.out index fa29abfd829..6e064e3807f 100644 --- a/src/test/regress/expected/strings.out +++ b/src/test/regress/expected/strings.out @@ -3441,6 +3441,32 @@ SELECT rpad('hi', 5, ''); hi (1 row) +-- whole repetitions of the pad string, a partial final repetition, and +-- fewer than one repetition +SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc'); + lpad | rpad +----------+---------- + abcabchi | hiabcabc +(1 row) + +SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc'); + lpad | rpad +-----------+----------- + abcabcahi | hiabcabca +(1 row) + +SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab'); + lpad | rpad +--------------+-------------- + abababababhi | hiababababab +(1 row) + +SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc'); + lpad | rpad +------+------ + ahi | hia +(1 row) + SELECT ltrim('zzzytrim', 'xyz'); ltrim ------- diff --git a/src/test/regress/sql/encoding.sql b/src/test/regress/sql/encoding.sql index 26caa93a5d5..3ea6e54e52d 100644 --- a/src/test/regress/sql/encoding.sql +++ b/src/test/regress/sql/encoding.sql @@ -37,6 +37,16 @@ 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; +-- multibyte pad strings: whole repetitions, a partial final repetition, and +-- fewer than one repetition +SELECT lpad(good, 7, 'é'), rpad(good, 7, 'é') FROM regress_encoding; +SELECT lpad(good, 12, 'éab'), rpad(good, 12, 'éab') FROM regress_encoding; +SELECT lpad(good, 5, 'éab'), rpad(good, 5, 'éab') FROM regress_encoding; +-- a lone lead byte in the pad string is an error if the padding reaches it +SELECT lpad(good, 7, test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT rpad(good, 7, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +SELECT lpad(good, 5, 'ab' || test_bytea_to_text('\xc3')), rpad(good, 6, 'ab' || test_bytea_to_text('\xc3')) FROM regress_encoding; +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; diff --git a/src/test/regress/sql/strings.sql b/src/test/regress/sql/strings.sql index 7d9c7275a02..04651a0a46f 100644 --- a/src/test/regress/sql/strings.sql +++ b/src/test/regress/sql/strings.sql @@ -1165,6 +1165,13 @@ SELECT rpad('hi', -5, 'xy'); SELECT rpad('hello', 2); SELECT rpad('hi', 5, ''); +-- whole repetitions of the pad string, a partial final repetition, and +-- fewer than one repetition +SELECT lpad('hi', 8, 'abc'), rpad('hi', 8, 'abc'); +SELECT lpad('hi', 9, 'abc'), rpad('hi', 9, 'abc'); +SELECT lpad('hi', 12, 'ab'), rpad('hi', 12, 'ab'); +SELECT lpad('hi', 3, 'abc'), rpad('hi', 3, 'abc'); + SELECT ltrim('zzzytrim', 'xyz'); SELECT translate('', '14', 'ax'); -- 2.17.1