bug: a zero-padded DECIMAL literal silently discards the other operand's fraction in a multiply (up to 50% wrong, no warning)
- Dominant language
- Go
- Stars
- 40.5k
- Forks
- 6.2k
- PR merge metrics
- PR metrics pending
Description
### Bug Report
#### 1. Minimal reproduce step (Required)
A decimal literal written with 32 or more integer digits — zero padding counts — corrupts the
multiplication it takes part in, whenever the other operand also has 32 or more integer digits.
No table is needed:
```sql
SELECT 99999999999999999999999999999999.000000
* 00000000000000000000000000000001.999999;
```
The right-hand literal is padded to 32 written integer digits; its **value** is `1.999999`.
It also happens with a stored column, which is the shape that matters in practice — a fixed-width
numeric field pasted into generated SQL:
```sql
CREATE TABLE d (id INT, b DECIMAL(38,6));
INSERT INTO d VALUES (1, '99999999999999999999999999999999.000000');
SELECT b * 00000000000000000000000000000001.999999 FROM d WHERE id = 1;
-- 99999999999999999999999999999999.000000000000 <- wrong, the multiplier became 1
SELECT b * 1.999999 FROM d WHERE id = 1;
-- 199999899999999999999999999999998.000001000000 <- right
```
The two statements differ only in how the same number is written.
#### 2. What did you expect to see? (Required)
```
199999899999999999999999999999998.000001000000
```
which is what MySQL 9.7.2 and MariaDB 13.0.2 both return for the identical statement, and what
TiDB itself returns as soon as the literal is written without the padding.
The declared result type is `DECIMAL(51,12)` — 39 integer digits and 12 fractional — so TiDB's
own type for the expression is far wider than the correct answer needs.
#### 3. What did you see instead (Required)
```
99999999999999999999999999999999.000000000000
```
The `.999999` of the multiplier is gone: the operand was used as `1`, not `1.999999`. That is a
**50% error** in the result, and **`SHOW WARNINGS` is empty**.
```
multiplier written as 0…01.999999 expected 199999899999999999999999999999998.000001000000
TiDB 99999999999999999999999999999999.000000000000 -50.00%
multiplier written as 0…01.500000 expected 149999999999999999999999999999998.500000000000
TiDB 99999999999999999999999999999999.000000000000 -33.33%
```
##### What is actually being discarded
It is not "the fraction of the result" — it is **the fractional digits of the second operand**,
before the multiply. Every observed result is exactly `operand1 × trunc(operand2)`:
| expression | TiDB returns | which is exactly |
|---|---|---|
| `0…01.000000 * 99…99.567891` | `99999999999999999999999999999999.000000000000` | `1 × 99…99` (trunc of operand 2) |
| `0…01.123456 * 99…99.567891` | `112345599999999999999999999999998.876544000000` | `1.123456 × 99…99` |
| `99…99.567891 * 0…01.123456` | `99999999999999999999999999999999.567891000000` | `99…99.567891 × 1` |
| `99…99.000000 * 0…01.999999` | `99999999999999999999999999999999.000000000000` | `99…99 × 1` |
##### Consequence: multiplication is not commutative
Because it is always *operand 2* that loses its fraction, `a * b` and `b * a` disagree. Both in
the same statement:
```sql
SELECT 00000000000000000000000000000001.000000 * 12345678912345678912345678912345.567891 AS lit_times_b,
12345678912345678912345678912345.567891 * 00000000000000000000000000000001.000000 AS b_times_lit;
```
```
lit_times_b = 12345678912345678912345678912345.000000000000 <- wrong
b_times_lit = 12345678912345678912345678912345.567891000000 <- right
```
The second form is correct only by luck: the fraction it throws away is `.000000`.
##### The threshold
Walking the zero padding on a literal whose value is exactly `1`, against a `99…9.567891` with
32 integer digits:
| written integer digits on the padded literal | result |
|---|---|
| 1 – 31 | correct |
| **32** | **wrong** |
| 33 – 40 | wrong |
and walking the other operand instead, with the padded literal fixed at 32 written digits:
| the other operand's integer digits | result |
|---|---|
| 1 – 31 | correct |
| **32** | **wrong** |
So the gate is 32 written integer digits on each side. 32 + 32 = 64 integer digits, plus the two
scales of 6, is 76 — past the 72 digits (`wordBufLen` 9 × `digitsPerWord` 9 = 81 word slots,
8 integer words + 1 fraction word here) that a `MyDecimal` can carry.
##### TiDB detects the genuine version of this and errors — it just misses this one
With operands whose values really do have 32 integer digits, TiDB raises, which is the right
thing to do:
```sql
SELECT 11111111111111111111111111111111.000000 * 99999999999999999999999999999999.567891;
-- ERROR 1265: Data truncated
```
The reported case has the same *written* width but a value of `1`, produces no error, and
silently returns a 50%-wrong number instead.
#### 4. What is your TiDB version? (Required)
Reproduced on current **master**:
```
Release Version: v9.0.0-beta.2.pre-2267-g02e5d7b3ec
Edition: Community
Git Commit Hash: 02e5d7b3ecf7615dfdfc238b272f39409bbd55f1
Git Branch: HEAD
UTC Build Time: 2026-09-17 01:50:35
GoVersion: go1.25.12
Store: unistore
```
That commit is `master` HEAD as of 2026-09-17T01:45:58Z (GitHub compare against `master`
reports `identical`, `ahead_by: 0`, `behind_by: 0`), from `pingcap/tidb:nightly` built
2026-09-17T01:50Z.
Also reproduced identically on the released **v8.5.8** (`8.0.11-TiDB-v8.5.8`), so this is not a
recent regression.
Contributor guide
Research direction
Start by running the minimal SELECT reproducer and the stored-column query described in the issue, then compare padded and unpadded decimal literals. Trace TiDB's decimal literal handling, multiplication path, and MyDecimal overflow or truncation behavior; done means preserving the second operand's fraction, maintaining commutativity, and reporting an error rather than silently returning a wrong result.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, sql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100