From 6dcad3ba7bd74e8799a4cf8a129f1918d49aa20d Mon Sep 17 00:00:00 2001 From: Sehrope Sarkuni Date: Thu, 24 Sep 2026 11:55:30 +0000 Subject: [PATCH v5 2/3] Copy whole repetitions of the pad string at once in lpad() and rpad() append_padding() called pg_mblen_range() and memcpy() once per character. Walk the pad string once to count its characters, write the whole repetitions with a new repeat_bytes() helper, and copy the partial final repetition as bytes. The pad string is validated as far as before, which is all of it when a full repetition is copied and otherwise only the characters copied. repeat_bytes() copies the string once, doubles the copied region until it is at least 16 kB, and then copies that region repeatedly, checking for interrupts between copies. Doubling needs only a few memcpy() calls for short strings, and the fixed block stays in cache for large results, which pure doubling did not. This also makes long lpad() and rpad() calls cancellable, which they were not before. Co-authored-by: Nathan Bossart Discussion: https://postgr.es/m/CAH7T-apj+pFg9bRkXVGfGejS2Uu4WzdMd7KoLKTW11mdsrt1Ew@mail.gmail.com --- src/backend/utils/adt/oracle_compat.c | 93 +++++++++++++++++++++++--- src/test/regress/expected/encoding.out | 37 ++++++++++ src/test/regress/expected/strings.out | 33 +++++++++ src/test/regress/sql/encoding.sql | 10 +++ src/test/regress/sql/strings.sql | 9 +++ 5 files changed, 174 insertions(+), 8 deletions(-) diff --git a/src/backend/utils/adt/oracle_compat.c b/src/backend/utils/adt/oracle_compat.c index e5238e44813..6fdbd595a9f 100644 --- a/src/backend/utils/adt/oracle_compat.c +++ b/src/backend/utils/adt/oracle_compat.c @@ -143,29 +143,106 @@ casefold(PG_FUNCTION_ARGS) } +/* + * Write count copies of the srclen-byte string src to dst and return a + * pointer past the last byte written. + * + * The first copy is written with memcpy(), then the region written so far + * is copied onto its own end, doubling it each time, until it is at least + * REPEAT_BYTES_BLOCK long. From there on that region is copied repeatedly. + * The doubling keeps the number of memcpy() calls small for short strings, + * and the fixed block, which is a whole number of copies and stays in + * cache, keeps the source reads cheap for large outputs. The caller must + * have checked that count * srclen bytes fit in dst. + */ +#define REPEAT_BYTES_BLOCK 16384 + +static char * +repeat_bytes(char *dst, const char *src, int srclen, int count) +{ + Size total = (Size) srclen * count; + Size written; + Size block; + + if (total == 0) + return dst; + Assert(srclen > 0 && count > 0); + + memcpy(dst, src, srclen); + written = srclen; + while (written < total && written < REPEAT_BYTES_BLOCK) + { + Size n = Min(written, total - written); + + memcpy(dst + written, dst, n); + written += n; + } + + block = written; + while (written < total) + { + Size n = Min(block, total - written); + + memcpy(dst + written, dst, n); + written += n; + CHECK_FOR_INTERRUPTS(); + } + + return dst + total; +} + /* * 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; + int nchars = 0; + int nrep; + int tail; + + Assert(padlen > 0 || m <= 0); + + if (m <= 0 || padlen <= 0) + return dst; - while (m--) + /* count the characters of one repetition, stopping at m */ + while (p < pend && nchars < m) { - int mlen = pg_mblen_range(p, pend); + p += pg_mblen_range(p, pend); + nchars++; + } - memcpy(dst, p, mlen); - dst += mlen; - p += mlen; - if (p == pend) /* wrap around at end of pad */ - p = pad; + /* whole repetitions only if the pad string was counted to its end */ + nrep = (p == pend) ? m / nchars : 0; + if (nrep == 0) + { + /* the loop above stopped after exactly m characters */ + memcpy(dst, pad, p - pad); + return dst + (p - pad); } - return dst; + dst = repeat_bytes(dst, pad, padlen, nrep); + + /* + * The remaining m - nrep * nchars characters are a prefix of pad that the + * loop above already checked, so measure them with the unbounded variant + * and copy them as bytes. + */ + p = pad; + for (tail = m - nrep * nchars; tail > 0; tail--) + p += pg_mblen_unbounded(p); + memcpy(dst, pad, p - pad); + + return dst + (p - pad); } /******************************************************************** 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..b588398d7fe 100644 --- a/src/test/regress/expected/strings.out +++ b/src/test/regress/expected/strings.out @@ -3441,6 +3441,39 @@ 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) + +-- padding longer than the 16 kB block that repeat_bytes() copies +SELECT rpad('', 50000, 'abc') = (SELECT string_agg('abc', '') FROM generate_series(1, 16666)) || 'ab'; + ?column? +---------- + t +(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..5db0df1c6ae 100644 --- a/src/test/regress/sql/strings.sql +++ b/src/test/regress/sql/strings.sql @@ -1165,6 +1165,15 @@ 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'); +-- padding longer than the 16 kB block that repeat_bytes() copies +SELECT rpad('', 50000, 'abc') = (SELECT string_agg('abc', '') FROM generate_series(1, 16666)) || 'ab'; + SELECT ltrim('zzzytrim', 'xyz'); SELECT translate('', '14', 'ax'); -- 2.55.0