Fix ALTER TABLE MODIFY TTL with DateTime causing data loss on 32-bit overflow#101793
Fix ALTER TABLE MODIFY TTL with DateTime causing data loss on 32-bit overflow#101793BoloniniD wants to merge 21 commits into
Conversation
alexey-milovidov
left a comment
There was a problem hiding this comment.
This is doubtful. A complex logic for a simple change. Maybe we can find a better way?
Without this, `widenTemporalColumns` stripped `Nullable` when rewriting `Date`/`DateTime` source columns. The analyzer then saw the column as non-null and constant-folded `isNull` / `ifNull`, silently changing TTL decisions for actually-`NULL` rows in both rows TTL and `DELETE WHERE`. `LowCardinality` is still dropped because `LowCardinality(DateTime64)` is rejected by `DataTypeLowCardinality`. The runtime cast in `ITTLAlgorithm::executeExpressionAndGetColumn` converts the original `LC` column to the widened type. Added stateless coverage for `Nullable(Date)` and `Nullable(DateTime)` TTL expressions exercising `ifNull` over the column.
Workflow files in this branch were stale or had been deleted as a workaround for GitHub's workflow-scope check. Restoring them to match `master` so the PR diff no longer contains those unrelated changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These changes to CI workflow / infrastructure files were unrelated to the purpose of this pull request and were introduced accidentally (stale base branch). Restoring them to match `master`.
|
Hello! Is this PR going to be finished and merged soon? |
# Conflicts: # src/Processors/TTL/ITTLAlgorithm.cpp
…xercises overflow Case 2 set `mutations_sync = 0`, and that leaked into the following cases. With the default `materialize_ttl_after_modify = 1`, those `ALTER ... MODIFY TTL` statements only enqueued an asynchronous `MATERIALIZE TTL` mutation, so the immediate `SELECT count()` could run before the TTL was evaluated and pass even if the 32-bit overflow path were still live. Keep `mutations_sync = 2` for the rest of the file so the materialization completes before the assertions. Addresses review feedback on the direct `ts + INTERVAL 100 YEAR` case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| ExpressionAndSets result; | ||
| auto ttl_string = ast->formatWithSecretsOneLine(); | ||
| auto syntax_analyzer_result = TreeRewriter(context).analyze(ast, columns); | ||
| auto syntax_analyzer_result = TreeRewriter(context).analyze(ast, widenTemporalColumns(columns)); |
There was a problem hiding this comment.
The widening here only helps after the TTL expression is evaluated, but existing parts may already have wrapped TTL metadata from the old DateTime evaluation. For example, a table created before this change with TTL ts + INTERVAL 100 YEAR can have table_ttl.max stored as a past timestamp while the rows themselves are still present. On the first TTL merge or MATERIALIZE TTL after upgrade, TTLTransform / TTLDeleteFilterTransform still check old_ttl_info.max and set the whole-part drop path before running the widened expression, so the part can be deleted without ever recalculating ts + INTERVAL 100 YEAR in the widened domain.
That leaves an upgrade path for the same silent data loss this PR is trying to close. Please make the whole-part shortcut recalculate or ignore stale Date / DateTime TTL infos for widened TTL descriptions, and add a regression that starts from stale wrapped part TTL metadata.
There was a problem hiding this comment.
This is a pre-existing stored-metadata concern in code this PR does not modify (TTLTransform / TTLDeleteFilterTransform). The exposure window is narrow: if the old narrow evaluation stored max in the past, those rows would already have been dropped by the old code before any upgrade — the only remaining case is a part inserted just before upgrade and not yet TTL-merged. Closing it requires recomputing ttl_infos or disabling the whole-part drop shortcut for widened TTL descriptions, which has merge-performance trade-offs, so it is a maintainer design call rather than a mechanical fix. See the status comment for the proposed direction.
Analyzing the TTL expression against `Date` / `DateTime` source columns widened to `Date32` / `DateTime64(0, tz)` fixes the silent 16/32-bit overflow (ClickHouse#101763), but it also rejects previously-valid TTLs whose functions accept only the narrow temporal types. For example `tumbleStart` requires `DateTime`, not `DateTime64`, so ALTER TABLE t MODIFY TTL tumbleStart(ts, INTERVAL 1 HOUR) + INTERVAL 1 DAY started failing with an illegal-argument-type exception, which would also break `ATTACH` / startup of legacy tables after an upgrade. Wrap the widened analysis in a fallback: if it throws, retry against the original column types. Such expressions explicitly operate in the narrow `Date` / `DateTime` domain and are out of scope for the overflow fix. The common `column + INTERVAL ...` case is unaffected and still analyzed in the widened 64-bit domain. Extract the analysis into a helper that clones the AST per attempt so a failed widened pass does not leave a half-rewritten AST for the fallback. Add Case 12 to `04077_datetime32_overflow` covering the `tumbleStart` compatibility regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| { | ||
| return analyzeExpressionAndSets(ast, widened_columns, context); | ||
| } | ||
| catch (const Exception &) // NOLINT(bugprone-empty-catch): intentional fallback to the narrow analysis below |
There was a problem hiding this comment.
The fallback here disables widening for the entire TTL expression. If widened analysis fails because one subexpression needs the narrow type, any later large interval also runs in the old narrow domain. For example TTL tumbleStart(ts, INTERVAL 1 HOUR) + INTERVAL 100 YEAR reaches this catch because tumbleStart rejects DateTime64, then the retry analyzes the final + INTERVAL 100 YEAR as addYears on DateTime; that path still casts the result back to UInt32, so a timestamp past 2106 can wrap into the past and expire rows.
Please keep the compatibility fallback from accepting expressions that still apply large interval arithmetic to narrow Date / DateTime results, or make the fallback narrower so the result of the narrow-only function is converted back to DateTime64 before the interval arithmetic is analyzed.
There was a problem hiding this comment.
This is not a regression versus pre-PR behavior: the fallback fires only when widened analysis throws (a function rejects DateTime64, e.g. tumbleStart). The targeted column + INTERVAL ... case never reaches it. Expressions like tumbleStart(ts, INTERVAL 1 HOUR) + INTERVAL 100 YEAR already wrapped before this PR and still wrap via the fallback — the same out-of-scope class @alexey-milovidov described above. Removing the fallback would break ATTACH of legacy tables using tumbleStart-style TTLs. The clean fix that would also cover this is throwing on overflow in addInterval itself; raised as a design decision in the status comment.
|
Status update (continue-pr) Merged current I did not expand the scope of the change. Three review threads are still open; here is where each stands and why I left the code as-is rather than adding logic:
Design decision for the maintainer. All three open items have the same shape: the widening approach fixes the reported case (#101763) but cannot, by construction, catch interval arithmetic applied to a narrow value produced inside the expression. The complete-and-simpler alternative @alexey-milovidov already named — throw on overflow directly in the
@alexey-milovidov @BoloniniD — which direction do you want? I will implement (b) if that is the call.
|
|
CI follow-up (continue-pr) The re-run after the
No code change is warranted for these. The net diff against |
LLVM Coverage Report
|

Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Fixes silent data loss when using large intervals with DateTime columns in TTL expressions due to 32-bit Unix timestamp overflow. Closes #101763
Documentation entry for user-facing changes
Note
Medium Risk
Changes TTL expression type analysis and runtime evaluation for
Date/DateTimecolumns, which directly affects TTL expiration decisions and data deletion/part cleanup behavior. While scoped to temporal types, incorrect casts or analyzer mismatches could change retention outcomes for some tables.Overview
Prevents silent TTL overflow on 16/32-bit
Date/DateTimecolumns by analyzing TTL expressions against widened temporal types (Date32/DateTime64(0, tz)), preservingNullablesemantics and droppingLowCardinalitywhere required.Updates TTL execution to cast required input columns to the types expected by the analyzed expression before running it, ensuring the runtime block matches the widened analysis view.
Adds a stateless regression test covering large-interval TTLs across rows/column/GROUP BY/recompress TTL modes, plus
LowCardinality/Nullabletemporal columns and a negative control for normal expiration.Reviewed by Cursor Bugbot for commit 43b6d26. Bugbot is set up for automated code reviews on this repo. Configure here.