[BugFix] Never raise a strict cast overflow error from a null row's data#74903
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a correctness bug in the native Parquet dictionary decoder where NULL rows could leave their backing data slots uninitialized, which can trigger false range-check failures during narrowing casts (e.g., under sql_mode='ALLOW_THROW_EXCEPTION'). The change ensures deterministic zero-filling for the null-heavy decode paths and adds both SQL and BE-unit regressions to guard against reintroduction.
Changes:
- Zero-initialize newly grown buffers in dict decode-with-nulls paths by switching from
resize_uninitialized()toresize(). - Add a deterministic BE unit test that poisons the destination buffer and verifies NULL slots are zero-filled (covers sparse and filter-forwarding branches).
- Add a Hive SQL regression reproducer for the original cast failure scenario.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
be/src/formats/parquet/encoding_dict.h |
Zero-fills nullable dict decode output slots to prevent uninitialized reads during casts; adds a test hook to control dict-size threshold. |
be/test/formats/parquet/dict_encoding_test.cpp |
Refactors existing tests slightly and adds a regression test validating NULL-slot zero-fill behavior. |
test/sql/test_hive/T/test_hive_parquet_dict_null_cast |
New SQL test that exercises dict-encoded, mostly-NULL Parquet data narrowed to INT under throw-checking mode. |
test/sql/test_hive/R/test_hive_parquet_dict_null_cast |
Expected results for the new SQL test. |
Comments suppressed due to low confidence (1)
be/test/formats/parquet/dict_encoding_test.cpp:82
- The RleEncoder does not flush pending state on scope exit (it has no destructor calling Flush), so the last run may not be written to
fs. This can make the test’s encoded stream incomplete and potentially hide regressions if the decoded count changes. Callencoder.Flush()after the loop to ensure the backing buffer contains all encoded values.
RleEncoder<DICT_CXX_TYPE> encoder(&fs, 32);
for (size_t i = 0; i < 4096; ++i) {
encoder.Put(i % 9 + 1, 10);
}
}
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: afc1ce85e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
stdpain
left a comment
There was a problem hiding this comment.
I suggest a better fix: Call NullableColumn::fill_null_with_default before importing the side and handling the cast. This change will not solve the root cause, because OLAP scans or columns calculated from an expression will also exhibit the same behavior.
I split the changes into two PRs. This one fixes the direct root cause. The next PR will address the cast issue after passing performance tests. |
A strict-mode numeric narrowing cast evaluated its overflow check on every row, including null rows whose data slot is undefined. When a producer left garbage there (e.g. the Parquet dictionary decoder), the check saw an out-of-range value and threw a spurious exception, failing a query that should succeed. Fix it at the cast layer: NullAwareInputCheckUnaryFunction only treats a non-null row's overflow as an error, so a null row's data can never raise an exception regardless of its source. The hot loop stays branchless and vectorizable (convert all rows, detect overflow via the non-throwing check masked by the not-null flag and OR-accumulated); the throwing check runs only on a cold path. This is an alternative to StarRocks#74903 and is also ~1.4-1.6x faster. Add unit tests and a microbenchmark, plus the SQLTester cases from StarRocks#74903. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A strict-mode numeric narrowing cast evaluated its overflow check on every row, including null rows whose data slot is undefined. When a producer left garbage there (e.g. the Parquet dictionary decoder), the check saw an out-of-range value and threw a spurious exception, failing a query that should succeed. Fix it at the cast layer: NullAwareInputCheckUnaryFunction only treats a non-null row's overflow as an error, so a null row's data can never raise an exception regardless of its source. The hot loop stays branchless and vectorizable (convert all rows, detect overflow via the non-throwing check masked by the not-null flag and OR-accumulated); the throwing check runs only on a cold path. This is an alternative to StarRocks#74903 and is also ~1.4-1.6x faster. Add unit tests and a microbenchmark, plus the SQLTester cases from StarRocks#74903. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@Mergifyio backport branch-4.0 |
|
@Mergifyio backport branch-3.5 |
✅ Backports have been createdDetails
Cherry-pick of 5847dd7 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally |
|
@Mergifyio backport branch-4.1 |
✅ Backports have been createdDetails
Cherry-pick of 5847dd7 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally |
✅ Backports have been createdDetails
Cherry-pick of 5847dd7 has failed: To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally |
Keep the branch-3.5 bench list and add only the new cast_overflow_check_bench entry introduced by #74903; the other bench targets from main do not exist on branch-3.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ata (#74903) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: wanpengfei-git <wanpengfei91@163.com>
…ata (#74903) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: wanpengfei-git <wanpengfei91@163.com>
…ata (StarRocks#74903) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: wanpengfei-git <wanpengfei91@163.com> Signed-off-by: Oliver Layer <o.layer@celonis.com>
Why I'm doing:
A strict-mode numeric narrowing cast (the
AllowThrowExceptionpath ofUNARY_FN_CAST_VALID,e.g.
cast(x AS INT)when an overflow must error instead of becoming NULL) evaluated the overflowcheck on every row of the input, including null rows. The data stored in a null row's slot
is undefined: when a producer leaves garbage there, the check sees an out-of-range value and throws
a spurious exception, failing a query that should succeed — the row is NULL and its value is
irrelevant.
What I'm doing:
NullAwareInputCheckUnaryFunction(be/src/exprs/unary_function.h). For a nullable input itconverts all rows but only treats a non-null row's overflow as an error; a null row's data
can never be the cause of an exception. The hot loop stays branchless and vectorizable (convert
every row, detect overflow with the non-throwing check AND-ed with the not-null mask and
OR-accumulated); the throwing check runs only on a cold path entered when a genuine non-null row
overflows.
AllowThrowException) branch ofUNARY_FN_CAST_VALIDto use it, passing thenon-throwing
NumberCheckfor hot-loop detection andNumberCheckWithThrowExceptionfor the colderror path.
Bonus: the new implementation is ~1.4-1.6x faster than the old one across nullable/non-nullable
shapes — it avoids materializing and scanning a separate null-mask column and keeps the loop
vectorizable (no per-row branch, no throw in the hot path). Benchmark numbers are in the bench file.
What type of PR is this:
Does this PR entail a change in behavior?
If yes, please specify the type of change:
Checklist:
Bugfix cherry-pick branch check: