Skip to content

dumpformat: fix corrupted DECIMAL values in dictionary-encoded parquet files (#70436) - #70459

Merged
ti-chi-bot[bot] merged 3 commits into
pingcap:release-nextgen-202603from
ti-chi-bot:cherry-pick-70436-to-release-nextgen-202603
Aug 12, 2026
Merged

ti-chi-bot[bot] merged 3 commits into
pingcap:release-nextgen-202603from
ti-chi-bot:cherry-pick-70436-to-release-nextgen-202603

Conversation

@ti-chi-bot

@ti-chi-bot ti-chi-bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

This is an automated cherry-pick of #70436

What problem does this PR solve?

Issue Number: close #70365

Problem Summary:

IMPORT INTO silently writes wrong values for repeated dictionary-encoded Parquet DECIMAL columns. Only the first occurrence of a dictionary entry is correct; every later row referencing the same entry gets a different value (0.00 in the reported witness). The job reports success and ADMIN CHECK TABLE passes, because both validate the KVs produced after the value was already corrupted.

Root cause is buffer ownership:

  1. types.MyDecimal.FromParquetArray documents its input as disposable and does the radix conversion by overwriting the byte slice in place (the two's-complement negation and the long division both write into buf). The oversized-value fallback getStringFromParquetByte does the same.
  2. arrow-go's dictConverter.Fill/Copy fill decoded values by assigning dict[idx], i.e. only the slice header. Every row referencing the same dictionary entry shares one backing array.
  3. The DECIMAL setters passed the decoded slice straight into the conversion, so converting the first row rewrote the dictionary entry, and later rows decoded from the already-consumed bytes.

What changed and how does it work?

Copy each value into a buffer owned by the setter before converting. The column iterator drives setters sequentially (columnIterator is documented as not usable in parallel), so one reused buffer per column keeps the copy allocation-free — no per-row allocation is added.

Both byte-array DECIMAL setters (BYTE_ARRAY and FIXED_LEN_BYTE_ARRAY) go through the shared helper, so the fix covers both physical types and both the direct MyDecimal path and the string fallback path for oversized values.

AI has also checked other code paths, this is the only path need to be fixed.

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Fix the issue that `IMPORT INTO` silently writes wrong values for repeated dictionary-encoded Parquet DECIMAL columns.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of dictionary-encoded decimal values during data conversion.
    • Ensured variable-length and fixed-length decimal values, including negative and oversized values, are decoded accurately and consistently.
  • Tests

    • Added coverage for dictionary-encoded decimal data across positive, negative, fixed-length, and oversized values.

joechenrh and others added 2 commits August 12, 2026 04:45
setDatumFromDecimalByte consumes its input in place (radix conversion and
two's-complement negation both write into the byte slice). Values decoded from
a dictionary-encoded page alias the shared dictionary entry, so converting the
first row rewrote the dictionary bytes and every later row referencing the same
entry decoded from the already-consumed buffer, silently producing a wrong
value (0.00 in the reported witness).

Copy each value into a buffer owned by the setter before converting. The
column iterator drives setters sequentially, so one reused buffer per column
keeps the copy allocation-free.

close pingcap#70365

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… test

Values of maximumDecimalBytes or longer take getStringFromParquetByte, which
consumes its input in place just like FromParquetArray, so add a dictionary
encoded column that reaches it. Verified it decodes 0.00 from row 1 onwards
when the copy is removed.

Also rename newDecimalByteSetter to getDecimalByteSetter to match the other
setter factories in the file, and assert the error from closing the reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. type/cherry-pick-for-release-nextgen-202603 labels Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0199efa6-c7e1-4513-b23b-7f0de0c079b4

📥 Commits

Reviewing files that changed from the base of the PR and between f09eb67 and ea7b872.

📒 Files selected for processing (1)
  • pkg/lightning/mydump/parquet_parser_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/lightning/mydump/parquet_parser_test.go

📝 Walkthrough

Walkthrough

The change prevents in-place decimal conversion from mutating dictionary-backed Parquet byte arrays. It adds coverage for repeated dictionary-encoded variable-length and fixed-length decimal values across 256 rows.

Changes

Parquet decimal decoding

Layer / File(s) Summary
Copy decimal byte arrays before conversion
pkg/lightning/mydump/parquet_type_converter.go
Variable-length and fixed-length decimal setters copy input bytes into an owned reusable buffer before conversion.
Validate repeated dictionary-encoded decimals
pkg/lightning/mydump/parquet_parser_test.go
The test verifies dictionary encoding and repeated positive, negative, fixed-length, and oversized decimal values across 256 rows.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • pingcap/tidb#70436: Modifies the corresponding decimal conversion helpers and adds an equivalent dictionary-encoded decimal regression test.
  • pingcap/tidb#70461: Modifies the same decimal conversion setters and adds equivalent dictionary-encoded Parquet decimal tests.

Poem

A rabbit found shared bytes in a row,
And stopped their changing flow.
Each decimal keeps its rightful face,
Through every dictionary place.
Hop, hop—256 values shine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the dumpformat DECIMAL corruption fix for dictionary-encoded Parquet files.
Description check ✅ Passed The description includes the issue, root cause, implementation, unit test coverage, side effects, and release note.
Linked Issues check ✅ Passed The changes address the linked bug by protecting decoded DECIMAL buffers and testing repeated dictionary values across supported byte-array types.
Out of Scope Changes check ✅ Passed The production change and regression test are directly related to fixing dictionary-encoded Parquet DECIMAL corruption.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/lightning/mydump/parquet_parser_test.go`:
- Around line 1094-1188: Add a focused non-dictionary DECIMAL test alongside
dictionary_encoded_decimal, covering both ByteArray and FixedLenByteArray
columns. Configure the Parquet writer/test column definitions to disable
dictionary encoding for these DECIMAL values, then assert the parser returns the
expected decimal strings for each row and type.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8542d6e-4588-442c-a483-8307320aa041

📥 Commits

Reviewing files that changed from the base of the PR and between 442c42f and f09eb67.

📒 Files selected for processing (2)
  • pkg/lightning/mydump/parquet_parser_test.go
  • pkg/lightning/mydump/parquet_type_converter.go

Comment on lines +1094 to +1188
t.Run("dictionary_encoded_decimal", func(t *testing.T) {
// Enough repeated rows that the writer keeps the dictionary page instead of
// falling back to PLAIN, so every row decodes to the same shared buffer.
const rows = 256
repeatedByteArray := func(val []byte) func(int) (any, []int16) {
return func(numRows int) (any, []int16) {
values := make([]parquet.ByteArray, numRows)
levels := make([]int16, numRows)
for i := range numRows {
values[i] = val
levels[i] = 1
}
return values, levels
}
}
repeatedFixedLenByteArray := func(val []byte) func(int) (any, []int16) {
return func(numRows int) (any, []int16) {
values := make([]parquet.FixedLenByteArray, numRows)
levels := make([]int16, numRows)
for i := range numRows {
values[i] = val
levels[i] = 1
}
return values, levels
}
}

pc := []testutils.ParquetColumn{
{
Name: "positive",
Type: parquet.Types.ByteArray,
Converted: schema.ConvertedTypes.Decimal,
Precision: 9,
Scale: 2,
Gen: repeatedByteArray([]byte{0x30, 0x39}), // 12345
},
{
Name: "negative",
Type: parquet.Types.ByteArray,
Converted: schema.ConvertedTypes.Decimal,
Precision: 9,
Scale: 2,
Gen: repeatedByteArray([]byte{0xcf, 0xc7}), // -12345
},
{
Name: "fixed_len",
Type: parquet.Types.FixedLenByteArray,
Converted: schema.ConvertedTypes.Decimal,
TypeLen: 4,
Precision: 9,
Scale: 2,
Gen: repeatedFixedLenByteArray([]byte{0x00, 0x00, 0x30, 0x39}), // 12345
},
{
// maximumDecimalBytes or longer, so this one takes the string
// fallback path, which consumes its input in place as well.
Name: "oversized",
Type: parquet.Types.ByteArray,
Converted: schema.ConvertedTypes.Decimal,
Precision: 75,
Scale: 2,
Gen: repeatedByteArray(append([]byte{0x00, 0x01}, make([]byte, 31)...)), // 2^248
},
}

dir := t.TempDir()
fileName := "test.decimal.dictionary.parquet"
require.NoError(t, testutils.WriteParquetFile(dir, fileName, pc, rows))

rdr, err := file.OpenParquetFile(filepath.Join(dir, fileName), false)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, rdr.Close())
})
for i := range pc {
cc, err := rdr.MetaData().RowGroup(0).ColumnChunk(i)
require.NoError(t, err)
require.True(t, cc.HasDictionaryPage(), "column %s is not dictionary encoded", pc[i].Name)
}

reader := newParquetParserForTest(context.Background(), t, dir, fileName, 0, FileMeta{})

expectedValues := []string{
"123.45", "-123.45", "123.45",
"4523128485832663883733241601901871400518358776001584532791311875309106626.56",
}
for range rows {
require.NoError(t, reader.ReadRow())
require.Len(t, reader.lastRow.Row, len(expectedValues))
for i, expectValue := range expectedValues {
s, err := reader.lastRow.Row[i].ToString()
require.NoError(t, err)
require.Equal(t, expectValue, s)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find byte-array DECIMAL test cases and their writer options.
rg -n -U -C 5 \
  'Type:\s*parquet\.Types\.(ByteArray|FixedLenByteArray),\s*\n\s*Converted:\s*schema\.ConvertedTypes\.Decimal' \
  pkg/lightning/mydump

# Find dictionary-related writer configuration and assertions.
rg -n -C 4 'Dictionary|dictionary|With.*Encoding|With.*Writer' \
  pkg/lightning/mydump/parquet_parser_test.go pkg/lightning/mydump

Repository: pingcap/tidb

Length of output: 13092


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Decimal-related tests and helpers ---'
rg -n -C 8 'Decimal|decimal|WithDictionaryFor|WriteParquetFile' \
  pkg/lightning/mydump/parquet_parser_test.go \
  pkg/lightning/mydump/parquet_writer.go \
  pkg/lightning/mydump/parquet_type_converter.go

printf '%s\n' '--- Existing non-dictionary test context ---'
sed -n '1580,1725p' pkg/lightning/mydump/parquet_parser_test.go

printf '%s\n' '--- Relevant parser test structure ---'
sed -n '1000,1215p' pkg/lightning/mydump/parquet_parser_test.go

Repository: pingcap/tidb

Length of output: 50368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

path = Path("pkg/lightning/mydump/parquet_parser_test.go")
text = path.read_text()
for match in re.finditer(r'(?s)\{.*?\}', text):
    block = match.group(0)
    if "Converted: schema.ConvertedTypes.Decimal" in block and (
        "parquet.Types.ByteArray" in block
        or "parquet.Types.FixedLenByteArray" in block
    ):
        start = text.count("\n", 0, match.start()) + 1
        end = text.count("\n", 0, match.end()) + 1
        print(f"lines {start}-{end}:")
        print(" ".join(line.strip() for line in block.splitlines()))
PY

printf '%s\n' '--- All dictionary-disable uses ---'
rg -n -C 3 'WithDictionaryFor\([^)]*, false\)' pkg/lightning/mydump

Repository: pingcap/tidb

Length of output: 1736


Add non-dictionary DECIMAL coverage.

The repository has no parser test for non-dictionary ByteArray or FixedLenByteArray DECIMAL values. Add a focused case that disables dictionary encoding for both DECIMAL types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/lightning/mydump/parquet_parser_test.go` around lines 1094 - 1188, Add a
focused non-dictionary DECIMAL test alongside dictionary_encoded_decimal,
covering both ByteArray and FixedLenByteArray columns. Configure the Parquet
writer/test column definitions to disable dictionary encoding for these DECIMAL
values, then assert the parser returns the expected decimal strings for each row
and type.

The cherry-pick landed the production change correctly, but the test
came from master's pkg/dumpformat/parquetfile, where the parquet test
helpers live in a testutils package and the parser meta type is called
FileMeta. On this branch both helpers are package-local and the meta
type is ParquetFileMeta, so the test did not compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (release-nextgen-202603@442c42f). Learn more about missing BASE report.

Additional details and impacted files
@@                     Coverage Diff                     @@
##             release-nextgen-202603     #70459   +/-   ##
===========================================================
  Coverage                          ?   76.2303%           
===========================================================
  Files                             ?       1937           
  Lines                             ?     541476           
  Branches                          ?          0           
===========================================================
  Hits                              ?     412769           
  Misses                            ?     128707           
  Partials                          ?          0           
Flag Coverage Δ
unit 76.2303% <100.0000%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 61.5065% <0.0000%> (?)
parser ∅ <0.0000%> (?)
br 48.7860% <0.0000%> (?)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ti-chi-bot ti-chi-bot Bot added approved needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Aug 12, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: D3Hunter, joechenrh

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels Aug 12, 2026
@ti-chi-bot

ti-chi-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

[LGTM Timeline notifier]

Timeline:

  • 2026-08-12 07:33:28.336003371 +0000 UTC m=+3204594.372098427: ☑️ agreed by D3Hunter.
  • 2026-08-12 07:34:22.707904616 +0000 UTC m=+3204648.743999672: ☑️ agreed by joechenrh.

@ti-chi-bot
ti-chi-bot Bot merged commit 85aefb4 into pingcap:release-nextgen-202603 Aug 12, 2026
18 checks passed
@ti-chi-bot
ti-chi-bot Bot deleted the cherry-pick-70436-to-release-nextgen-202603 branch August 12, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved lgtm release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. type/cherry-pick-for-release-nextgen-202603

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants