| From: | PG Bug reporting form <noreply(at)postgresql(dot)org> |
|---|---|
| To: | pgsql-bugs(at)lists(dot)postgresql(dot)org |
| Cc: | gowthamdoma18(at)gmail(dot)com |
| Subject: | BUG #19771: to_char(): NUL byte in result for formats with a trailing decimal point (regression from ec352b37d) |
| Date: | 2026-10-18 20:40:57 |
| Message-ID: | 19771-4f173b891525020c@postgresql.org |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
The following bug has been logged on the website:
Bug reference: 19771
Logged by: Gowtham Reddy
Email address: gowthamdoma18(at)gmail(dot)com
PostgreSQL version: 19rc1
Operating system: Linux x86_64
Description:
Hello,
On master (tested at 45fc51fcbf45), to_char() for numbers returns a text
value containing a NUL byte when the format has a decimal point with no
digit positions after it:
SELECT encode(textsend(to_char(7, '9.')), 'hex');
-- master: 203700 REL_19: 2037
SELECT encode(textsend(to_char(7, '9.PL')), 'hex');
-- master: 2037002b REL_19: 2037
SELECT length(to_char(7, '9.')), octet_length(to_char(7, '9.'));
-- master: 2, 3
SELECT to_char(7, '9.') = ' 7';
-- master: false REL_19: true
SELECT encode(textsend(to_char('100'::numeric, 'FM999.')), 'hex');
-- master: 31303000
The NUL byte is also written to tables: CREATE TABLE t AS SELECT
to_char(7, '9.PL') stores 2037002b. The existing regression query in
numeric.sql (SELECT to_char('100'::numeric, 'FM999.')) returns '100\0' on
master but still passes, because psql stops printing at the NUL.
Bisected to:
ec352b37d Replace formatting.c's fixed-size output buffers with
StringInfos.
Its parent 28c5d0724 returns 2037 for the second query.
The underlying problem is older. In NUM_numpart_to_char(), the "Write
Digits" branch copies *Np->input_p even when the number string is already
exhausted, so it copies the terminating '\0'. With the old fixed-size
buffer that NUL ended the C string, so in released branches everything
after the trailing decimal point is silently dropped, including the sign:
SELECT to_char(-7, '9.MI'); -- REL_19: '7' (sign lost)
SELECT to_char(-7, '9.PR'); -- REL_19: '<7' (closing bracket lost)
SELECT to_char(7, '9.PL'); -- REL_19: ' 7' (PL lost)
SELECT to_char(7, '9."x"'); -- REL_19: ' 7' (literal lost)
With the StringInfo output buffer the NUL is kept, and the suffix appears
after it.
Proposed patch (attached, v1-0001): do not emit the string terminator as a
digit. It also adds a regression test to numeric.sql, which fails without
the fix. The core change is:
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -5659,7 +5659,7 @@ NUM_numpart_to_char(NUMProc *Np, int id)
NUM_EMITC('0');
}
}
- else
+ else if (*Np->input_p != '\0')
{
NUM_EMITC(*Np->input_p); /*
Write DIGIT */
Np->num_in = true;
With this, master returns ' 7', ' 7+', '7-', '<7>', ' 7x' for the queries
above, ' 1,234' for to_char(1234, '9,999.'), and '100' for the numeric.sql
case. Formats without a trailing decimal point are unchanged ('9.9',
'FM9.9', '99.99PL', '99PR'). make check passes (239/239) on master
45fc51fcbf45 with the patch.
This changes the result of e.g. to_char(-7, '9.MI') from '7' to '7-'
compared with released branches. I think that is the correct result, but
you may prefer to handle the back-branches differently.
A regression test that would have caught it:
SELECT octet_length(to_char(7, '9.')), to_char(-7, '9.MI'),
to_char(-7, '9.PR');
We found this with a cross-version differential fuzzer (random to_char,
to_number and to_date calls on REL_19_STABLE vs master), written for a
university course project (CS-5187).
Regards,
Gowtham
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Gowtham Reddy | 2026-10-18 20:47:07 | Re: BUG #19771: to_char(): NUL byte in result for formats with a trailing decimal point (regression from ec352b37d) |
| Previous Message | PG Bug reporting form | 2026-10-18 13:58:03 | BUG #19770: CREATE TABLE (LIKE ...) PARTITION BY copies a NO INHERIT not-null constraint; NULLs then go into par |