Fix false-positive "column deleted" stale check for timestamp-index derived columns#18294
Conversation
…erived columns Timestamp-index derived columns ($col$GRANULARITY) are physically materialized in segment files when a TimestampConfig is active, but are not first-class columns in the user-facing schema. When the timestamp index is later removed from the table config, applyTimestampIndex no longer injects the derived column into the IndexLoadingConfig schema, causing isSegmentStale to incorrectly report "column deleted: $col$GRANULARITY" and trigger an unnecessary segment reload. The fix skips the "column deleted" verdict for $col$GRANULARITY columns when the base column is still present in the schema (confirming the column is a timestamp-derived orphan rather than a genuine user-schema deletion). When the base column is also absent, the guard does not activate, preserving correct behavior for user-defined columns whose names happen to match the pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #18294 +/- ##
============================================
+ Coverage 63.58% 63.61% +0.03%
Complexity 1659 1659
============================================
Files 3245 3245
Lines 197441 197445 +4
Branches 30564 30566 +2
============================================
+ Hits 125536 125599 +63
+ Misses 61856 61809 -47
+ Partials 10049 10037 -12
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| // We only skip when the base column is still present in the schema; if the base column was also removed, | ||
| // the column is not a timestamp-derived orphan and must still trigger "column deleted". | ||
| if (TimestampIndexUtils.isValidColumnWithGranularity(columnName)) { | ||
| String baseColumn = columnName.substring(1, columnName.indexOf('$', 1)); |
There was a problem hiding this comment.
There must be util for this? If not let's add.
|
At high level, when a timestamp index is removed, the segment should be count as stale right? Then a reload can clean up the old columns. This is similar to when we remove an inverted index |
|
Yes, I agree we should still mark this segment as stale. |
|
@Jackie-Jiang @noob-se7en that makes sense. Let me take another look. My initial take was that the stale reason should be flagged by the timestamp index check and not come up as a column removed operation. Its confusing as the schema will not have the column. Also I ran few flows around the timestamp index and the stale reason is hard to understand as well as for few cases it is not fixed even after a segment reload. Steps:
Looks like the right fix would be to add a dedicated timestamp index validation and emit the right reason of staleness Goal is to make the stale API meaningful as well as actionable through something like segment reload. If a stale reason cannot be addressed by reload we should have a way to skip these as typical usecase involve hitting this stale check and proceed to run reload if needed to handle things like preventing server restart time, etc. |
|
The comments were still open 🥲 @gortiz |
|
We can move the discussion to #18310 @noob-se7en @Jackie-Jiang |
…n isSegmentStale (#18310) The previous fix (#18294) added a narrow guard inside the "column deleted" branch to suppress false positives when a timestamp index is removed. This was incomplete: - Removing a timestamp index should mark the segment stale (so SegmentPreProcessor can clean up the orphaned $col$GRANULARITY columns), but the old guard silently skipped them. - Adding a new granularity or configuring a timestamp index for the first time was misreported as "column added" instead of a dedicated timestamp index reason. - Existing derived columns in the segment could trigger spurious "range index changed" verdicts on every reload because applyTimestampIndex injects them into the range index config. This commit replaces the guard with a dedicated feature-level check (following the StarTree and MultiColumnText pattern). Before the generic per-column loop: 1. Compare the set of $col$GRANULARITY columns expected by the current table config against those physically present in the segment. Any mismatch returns "timestamp index changed", correctly covering add/remove/granularity changes. 2. The generic "column added" check now excludes $col$GRANULARITY columns (handled by step 1). 3. The per-column loop skips all $col$GRANULARITY columns entirely, preventing false positives on "column deleted" and "range index changed". Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
$col$GRANULARITY) are materialized as physical segment columns when aTimestampConfigis active, but are not present in the user-facing schema. When the timestamp index is later removed from the table config,applyTimestampIndexno longer injects the derived column into theIndexLoadingConfigschema, causingisSegmentStaleto fire"column deleted: $col$GRANULARITY"and trigger an unnecessary segment reload.$col$GRANULARITYcolumns when the base column is still in the schema (confirming this is a timestamp-derived orphan, not a genuine user-schema deletion). If the base column is also absent the guard does not activate, so user-defined columns whose names happen to match the pattern are still correctly caught.$col$GRANULARITY-named column that is genuinely deleted.Root cause
FieldSpec.isVirtualColumn()returnstrueonly when_virtualColumnProvideris set — atransformFunctionalone does not make a column virtual. So$col$DAYadded byapplyTimestampIndexlands inschema.getPhysicalColumnNames(), and the segment also stores it as a physical column (withisAutoGenerated=false). When theTimestampConfigis removed, the schema no longer contains the derived column but the segment still does, triggering the false stale verdict.Test plan
BaseTableDataManagerNeedRefreshTest— all 43 tests pass (./mvnw -pl pinot-core -am -Dtest=BaseTableDataManagerNeedRefreshTest -Dsurefire.failIfNoSpecifiedTests=false test)spotless:apply,license:format,checkstyle:check,license:checkall pass onpinot-core🤖 Generated with Claude Code