[BugFix] Handle NOT NULL -> nullable flat-JSON columns in compaction read path (fix JsonMergeIterator CHECK crash) (backport #75680)#75749
Merged
Conversation
…read path (fix JsonMergeIterator CHECK crash) (StarRocks#75680) Signed-off-by: Srihith Garlapati <srihith.garlapati@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Srihith Garlapati <srihith.garlapati@gmail.com>
kevincai
approved these changes
Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why I'm doing:
We hit a BE/CN crash loop on a shared-data cluster after a user ran
ALTER TABLE t MODIFY COLUMN c json NULLon a flat-JSON column that was originallyNOT NULL.Fast schema evolution makes that ALTER metadata-only, so the old segments on object storage are
never rewritten — they stay physically NOT NULL with no null sub-stream. The next background
compaction then reads those segments into a now-nullable output column and dies here:
The reader has
_null_iter == nullptr(segment has no null stream) but the output is nullable(
null_column != nullptr), so the CHECK fires and SIGABRTs the whole process. Compaction retries,crashes again, and the node never recovers. Reproduced on 4.0.12.
The thing is, this state is perfectly legal. A NOT NULL segment can't contain nulls, so the reader
just needs to fill the null map with "not null" instead of aborting.
What I'm doing:
Fix is read-side only, all in
be/src/storage/rowset/json_column_iterator.cpp. No writer, FE,thrift or proto changes.
All 6 read methods on
JsonMergeIteratorandJsonFlatColumnIterator(bothnext_batchoverloads and
fetch_values_by_rowidon each) now handle the two mismatch cases instead ofCHECKing:
append not-null flags to the output's null column for exactly the rows produced
(
json_column->size() - before), thenupdate_has_null(). Sameappend_value_multiple_times(&NOT_NULL, n)idiom the flat-JSON writer already uses.Status::InternalErrorinstead of killing the process.While in there I fixed two adjacent problems in the same family:
JsonFlatColumnIterator::next_batch(size_t*)never had the CHECK at all and would silentlyunder-fill the null column in release builds, and
JsonFlatColumnIterator::fetch_values_by_rowiddown_cast the destination toJsonColumnwhenever the segment had no null stream — UB if the destination was actually nullable. Also moved
check_or_die()behind the success path, since running it on a half-filled column after a failedread would just abort in a different place.
Tests added to
FlatJsonColumnCompactTest:CompactJsonNotNullToNullableSchemaEvolution— NOT NULL flat segment read into a nullablecolumn through all three read paths. This SIGABRTs without the fix. Asserts row count, that
every synthesized null byte is 0, and that the JSON values round-trip.
CompactJsonMixedNullabilitySegments— NOT NULL + nullable + NOT NULL segments merged into oneoutput, so the backfill also runs against a non-empty destination and sits next to a real NULL.
CompactJsonNullableSegmentToNonNullableOutputError— the inverse mismatch returnsInternalErroron all three paths instead of crashing.Scope note: only the flat-JSON read paths (
JsonMergeIterator/JsonFlatColumnIterator)couple the segment's stored nullability to the output column like this. Non-flat JSON segments
are read through
ScalarColumnIterator, and dynamic flattening (JsonDynamicFlatIterator)reads through a
clone_empty()proxy -- both maintain the output null column through theColumn append API and have no nullability-mismatch coupling, so they need no change.
The mismatch policy lives in two file-local helpers (
read_json_null_stream/finish_json_read) shared by all six read methods, so the planned ARRAY/MAP follow-up canreuse the same shape instead of copy-pasting it.
One heads-up for reviewers:
array_column_iterator.cpp(3x) andmap_column_iterator.cpp(1x)have the exact same CHECK and the same crash vector for ARRAY/MAP columns after the same kind of
ALTER. I'm deliberately not touching them here to keep this backportable and easy to review —
follow-up PR coming.
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:
(3.4 and 3.3 need manual backports — same as the earlier compaction crash fixes #63553 / #70539.)
This is a manual backport of pull request #75680 to branch-4.0. The automatic Mergify backport (#75724) could not apply cleanly because this branch predates the mutable-column accessor refactor, so the conflicts were resolved by hand (same adaptation in both: NullableColumn::data_column().get()/null_column().get() instead of the *_raw_ptr() accessors, and Columns-based test helpers). Verified locally: the full FlatJsonColumnCompactTest suite passes in an ASAN build on this branch.