Skip to content

Do not consult the query context when building a CAST - #114769

Merged
Avogar merged 4 commits into
ClickHouse:masterfrom
groeneai:cast-resolver-expired-context
Aug 18, 2026
Merged

Do not consult the query context when building a CAST#114769
Avogar merged 4 commits into
ClickHouse:masterfrom
groeneai:cast-resolver-expired-context

Conversation

@groeneai

@groeneai groeneai commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fixed a LOGICAL_ERROR: Context has expired exception when a stored expression containing accurateCastOrDefault or a to<Type>OrDefault function is evaluated after the query that created it has finished, for example a MATERIALIZED column default on INSERT or a mutation expression.

Description

Related: #109946

CastOverloadResolverImpl inherited WithContext, which holds only a weak_ptr<const Context>, and its buildImpl passed getContext() to createFunctionBaseCast. That is safe only during analysis, while a caller still holds a strong ContextPtr.

#109946 made FunctionCastOrDefault store that resolver as a member and call build() at execute time. The ActionsDAG keeps the resolver alive but nothing keeps its context alive, and both affected paths build the DAG from a function-local Context::createCopy that dies on return (inplaceBlockConversions.cpp:230 for materialized-column defaults, MutationsInterpreter.cpp:1762 for mutations). Evaluating such an expression later throws Context has expired: an exception in release builds, a server abort under debug and sanitizers.

This captures the conversion settings in the resolver's constructor, while the context is provably alive, and has buildImpl consume that snapshot. FunctionCast used its ContextPtr for nothing but settings(context, ...), so the snapshot moves one step earlier in the same call chain rather than being introduced. The resolver now holds no context at all, so the WithContext exception for this file in various_checks.sh is removed.

The two build paths keep their distinct DateTimeOverflowBehavior: Saturate for createInternalCast, Ignore for buildImpl. Sharing one snapshot between them would silently change date-time conversion.

Since a snapshot replaces a live context, every context-derived conversion setting was compared before and after and is byte-identical, including all three date_time_overflow_behavior modes, cast_keep_nullable, precise_float_parsing, the IPv4/IPv6 error settings and timezone substitution. 04510_accurateCastOrDefault_settings from #109946 passes unchanged. The new test aborts a master server at CastOverloadResolver.cpp:195 and passes with this change, 50/50 green under randomized settings.

The defect rides along with #109946, so it is present wherever that landed: 26.7 (#114689, merged); the 25.8, 26.3 and 26.5 backports are still open.

CI provenance

Logical error: Context has expired, STID 1805-460a, first seen 2026-08-13 22:45:16 UTC, about four hours after #109946 merged. The frame pair CastOverloadResolverImpl::buildImpl + Context has expired has no earlier occurrence in the 90 days CIDB retains.

when (UTC) check where
2026-08-14 02:20:39 Stress test (arm_release) master, 17d2798b9626
2026-08-14 01:49:18 AST fuzzer (amd_debug, targeted) unrelated PR
2026-08-14 00:02:12 Stress test (amd_tsan) unrelated PR
2026-08-13 22:50:06 AST fuzzer (amd_debug, targeted, old_compatibility) unrelated PR
2026-08-13 22:45:16 AST fuzzer (arm_asan_ubsan) unrelated PR

The master row is bucketed as STID 1805-4810 because an STID is a stack hash, but it carries both discriminating frames and 17d2798b9626 contains #109946.


Workflow [PR]
Sync PR [sync-upstream/pr/114769]

Version info

  • Merged into: 26.8.1.1625 (included in 26.8 and later)

groeneai and others added 3 commits August 14, 2026 02:27
CastOverloadResolverImpl inherited WithContext, which holds only a
weak_ptr<const Context>, and buildImpl passed getContext() to
createFunctionBaseCast. That is safe only during analysis, while a caller
still holds a strong ContextPtr.

PR ClickHouse#109946 made FunctionCastOrDefault store that resolver as a member and
call build() at execute time. The ActionsDAG keeps the resolver alive but
nothing keeps its context alive, and both affected paths build the DAG from
a function-local Context::createCopy that dies on return
(inplaceBlockConversions.cpp for materialized-column defaults,
MutationsInterpreter.cpp for mutations). Evaluating such a stored expression
later throws LOGICAL_ERROR "Context has expired": an exception in release
builds, a server abort under debug and sanitizer builds. The AST fuzzer hit
it on two unrelated PRs within five minutes of that merge, and the signature
has no prior occurrence in 90 days of CI history.

Capture the conversion settings in the resolver's constructor, while the
context is alive, and have buildImpl consume that snapshot. FunctionCast used
its ContextPtr for nothing but building the same FunctionConvertSettings, so
this moves an existing snapshot one step earlier in the same call chain
rather than introducing a new one. The resolver now holds no context at all,
so the WithContext exception for this file in various_checks.sh is removed.

The two build paths keep their distinct DateTimeOverflowBehavior: Saturate
for createInternalCast, Ignore for buildImpl, the sentinel that defers to
date_time_overflow_behavior. Sharing one snapshot between them would
silently change date-time conversion, so they are built separately.

The snapshot is threaded as a shared_ptr so that CastOverloadResolver.cpp can
keep forward-declaring the factory instead of including the whole of
FunctionsConversion.h.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both mutation arms asserted 16/120, a value that already held before their
mutation executed: the MATERIALIZE COLUMN arm read a column whose stored bytes
and current default expression agreed, and the UPDATE arm recomputed the values
the INSERT had already written. Neither could tell a mutation that applied from
one that was skipped, so they measured nothing about the mutation reach path
they exist to cover.

Give each arm a discriminating before value. The MATERIALIZE COLUMN arm now
redefines the default with a metadata-only MODIFY COLUMN, so the stored bytes
(sum 120) differ from what the current expression produces, and only a mutation
that actually runs can reach 16120. The UPDATE arm inserts a -1 sentinel
(sum -16) that the UPDATE must overwrite to reach 120. Each arm gets its own
table so they cannot mask each other.

Verified by mutation rather than by assertion: deleting either statement makes
the test fail with a value diff (16 120 where 16 16120 is expected, and
16 -16 where 16 120 is expected).
Both reproduction arms of the test only reach the defect with the analyzer
enabled, so on an old-analyzer CI flavour the test passed even with the fix
reverted.

The expiring function-local context exists only on the analyzer branch of
each path. inplaceBlockConversions.cpp:280-283 (and the same gate in
evaluateMissingDefaults at :311-315) calls createExpressionsAnalyzer, which
takes a Context::createCopy that dies at return; the other branch,
createExpressions, forwards the caller's longer-lived context.
MutationsInterpreter.cpp:1758-1762 likewise creates the expiring copy only
under use_analyzer, while the old path at :2066-2072 hands ExpressionAnalyzer
the stored context.

tests/config/install.sh:297 symlinks users.d/analyzer.xml when
USE_OLD_ANALYZER=1, and that file sets allow_experimental_analyzer to 0 in the
default profile. Such jobs exist, and one of the recorded sightings is on
AST fuzzer (amd_debug, targeted, old_compatibility). A session SET is enough
for the mutation half as well, because use_analyzer_for_mutations is not set
anywhere under tests/config or ci, so shouldUseAnalyzerForMutations falls
through to the session setting.

Verified against the pre-fix binary with the default profile forced to the old
analyzer: with this line the test aborts with 'Context has expired' at
CastOverloadResolver.cpp:195, without it the test passes. enable_analyzer
already defaults to 1, so the reference is unchanged.
@groeneai

Copy link
Copy Markdown
Collaborator Author
Internal second-model review (click to expand)

Three review rounds on this change: an independent cold read of the code plus an
independent second-model pass each round, adjudicated against the recorded evidence.
The final round returned no findings from either.

# Finding Severity Verdict
04527 did not pin the analyzer, so both reproduction arms would be skipped on an old-analyzer CI flavour and the test would pass with the fix reverted blocker to test liveness AGREE, fixed. Verified two-sided on the pre-fix binary under a forced old-analyzer profile: the test aborts with the pin and passes without it, so the pin is what keeps the test live
Both mutation arms asserted a value that was already true before their mutation ran, so neither could detect a mutation that never executed major AGREE, fixed. Each arm now has differing before/after values on its own table, and deleting either mutation statement reddens with a value diff
⚠️ Provenance was missing from the description: the linked pull request sat inside an HTML comment, and no report URL, check name or failure identifier was rendered major AGREE, fixed. A rendered relationship line and the five canonical report URLs were added
💡 Changelog entry should open Fixes rather than Fixed nit DISAGREE. CHANGELOG.md has 1089 Fix, 405 Fixed and 48 Fixes; for 26.7 alone it is 36 / 31 / 0
💡 The constructor now builds a settings snapshot even for a resolver that is never built nit DISAGREE. FunctionCastOrDefault builds per block, and each build previously constructed the same struct from the live context, so N constructions collapse to one plus N copies. The construct-and-discard sites are all analysis-time
💡 getFormatSettings can throw, so that throw moves from build() to construction nit DISAGREE. Same error code and message on every path that reaches it, and system.functions already catches it
💡 Leaving the test untagged keeps it under randomized settings nit DISAGREE. Blanket opt-out tags are a last resort; none of the settings this test asserts is randomized by the runner, and it is green 50/50

Session id: cron:clickhouse-review-slot-8:20260814-063002

@groeneai

Copy link
Copy Markdown
Collaborator Author
Pre-PR validation gate (click to expand)
# Question Answer
a Deterministic repro? Yes. 04527_cast_resolver_expired_context against a master server FAILs every run (rc=32, server aborts, Context has expired at CastOverloadResolver.cpp:195:13, stack through castOrDefault.cpp:159 and InsertDependenciesBuilder::createPreSink:1800). Also reproduces in clickhouse local (rc=134) on two independent shapes.
b Root cause explained? CastOverloadResolverImpl held only a weak_ptr<const Context> via WithContext and dereferenced it in buildImpl. #109946 made FunctionCastOrDefault store that resolver and call build() at execute time, while both reach paths build the DAG from a function-local Context::createCopy that dies on return, so the weak reference is dead when the stored expression runs.
c Fix matches root cause? Yes. The context dereference is removed at its source by snapshotting the conversion settings at construction, while the context is provably alive. No guard, no widened bound, no fallback context (a context.lock() fallback was rejected as a wrong-results risk).
d Test intent preserved / new tests added? New regression test covering both reach paths (materialized-column default on INSERT, MATERIALIZE COLUMN/UPDATE mutation) plus the settings, wrapper and boundary matrix. No existing test weakened or removed; 04510_accurateCastOrDefault_settings unchanged and passing.
e Both directions demonstrated? Yes, through clickhouse-test: FAIL with a server abort on master, OK with the change, and 50/50 green under randomized settings.
f Fix is general across code paths? All 7 IFunctionOverloadResolver+WithContext classes re-enumerated. CastOverloadResolverImpl was the only one whose resolver is stored in a long-lived object; the fix removes the dereference for every one of its callers rather than the one stored carrier. The ~10 IFunctions holding factory-obtained resolvers are safe via FunctionToOverloadResolverAdaptor::buildImpl, which builds from types only and holds no context.
g Fix generalizes across inputs (params/datatypes/wrappers)? A/B compared master vs fix, byte-identical on: all 12 context-derived FunctionConvertSettings fields (including all three date_time_overflow_behavior modes on CAST, _CAST and accurateCastOrNull), timezone substitution, wrappers (Nullable, LowCardinality, LC(Nullable), Const, Array, Map) and boundaries (empty, len 1, min/max, overflow, NULL). Those SQL entry points all reach buildImpl, i.e. the Ignore snapshot; the Saturate snapshot belongs to createInternalCast, which has no SQL entry point and is covered by the pre-existing 03271_date_to_datetime_saturation and 02900_date_time_check_overflow. Because Ignore is the default sentinel that defers to the setting, the date_time_overflow_behavior='ignore' arm would redden if the two snapshots were ever shared.
h Backward compatible? (maintainer-approved exception only) Yes. No setting added or default changed, no serialization format touched, so no SettingsChangesHistory.cpp entry is required. The only external-facing edit is removing a now-stale style-check exception.
i Invariants and contracts preserved? The snapshot is taken from the same context, at the same moment, that FunctionCast already consulted, so conversion semantics are unchanged (verified byte-identical). createInternalCast keeps Saturate/internal=true and buildImpl keeps Ignore; the two are never shared. getReturnTypeImpl never touched the context and is unmodified, so timezone substitution and cast_keep_nullable are untouched (both asserted). The change only removes shared mutable state, so no locking or concurrency contract is affected; one shared_ptr per resolver, shared by every cast it builds (3M-row toInt64OrDefault: 0.179 s master vs 0.166 s fix).

Session id: cron:clickhouse-review-slot-10:20260814-023002

@clickhouse-gh

clickhouse-gh Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [0b50647]

Summary:

job_name test_name status info comment
Finish Workflow FAIL
python3 ./ci/jobs/scripts/workflow_hooks/new_tests_check.py FAIL IGNORED

AI Review

Summary

This PR snapshots FunctionConvertSettings inside CastOverloadResolverImpl while the constructing Context is still alive, so stored accurateCastOrDefault / to<Type>OrDefault expressions no longer need to lock an expired query context when they build the cast at execution time. The split between query-time date_time_overflow_behavior and internal saturating casts is preserved, the new stateless regression test covers the affected stored-expression paths plus the settings-sensitive behavior, and I did not find an unresolved correctness or evidence gap in the current head.

Final Verdict
  • Status: ✅ Approve

LLVM Coverage Report

Metric Baseline Current Δ
Lines 86.80% 86.80% +0.00%
Functions 92.00% 92.00% +0.00%
Branches 79.20% 79.20% +0.00%

Changed lines: Changed C/C++ lines covered: 34/34 (100.00%) · Uncovered code

Full report · Diff report

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Aug 14, 2026
@groeneai

Copy link
Copy Markdown
Collaborator Author

cc @Ergus @Algunenano, could you review this? CastOverloadResolverImpl held only a weak_ptr to the query context and dereferenced it in buildImpl; since #109946 that resolver is stored and built at execute time, so a materialized-column default or a mutation expression evaluates it after the context is gone and throws Context has expired. This snapshots the conversion settings in the constructor instead, keeping the separate Ignore and Saturate behaviours.

@clickhouse-gh

clickhouse-gh Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Build profile diff (arm_release)

Comparing 0b50647b6 with master d59f2be34 (stripped binary size, per-symbol sizes and ThinLTO time; compile times per translation unit against the most recent warmup build that recompiled it).

⚠️ Significant changes: object file sizes.

Binary sizes
Binary Master PR Δ
programs/clickhouse-stripped 700.94 MiB 698.11 MiB -2.83 MiB (-0.40%)

Only the stripped binary is compared: the official master build keeps debug symbols while PR builds strip them, so the other binaries differ by construction.

Object file sizes ⚠️

19 object files changed (+451.48 KiB total), 0 added.

Object file Master PR Δ
src/CMakeFiles/dbms.dir/Interpreters/Aggregator.cpp.o 11.18 MiB 11.53 MiB +366.26 KiB (+3.20%)
src/CMakeFiles/dbms.dir/Processors/Transforms/LimitByTransform.cpp.o 1.07 MiB 1.10 MiB +32.88 KiB (+3.00%)
src/CMakeFiles/dbms.dir/Processors/Transforms/NegativeLimitByTransform.cpp.o 784.98 KiB 802.46 KiB +17.48 KiB (+2.23%)

737 more object files are built by the master warmup baseline only (it builds every object-file target, a pull request build only clickhouse-bundle) and not compared.

Compile time of recompiled translation units

48 translation units recompiled, 318 s compile time in total, 48 of them have a recent master baseline.

Job report

@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger - da4e7e6

CI is fully finished on this head and there are no failures to own.

Gates: no check queued/in_progress/pending; Config Workflow = success, Finish Workflow = success (2026-08-15T06:54:39Z); >20 min elapsed for CIDB ingestion.

Coverage verified from the praktika report rather than the skip ratio: result_pr.json top-level OK, 177 leaves, 160 OK / 17 SKIPPED / 0 DROPPED. Every skip carries a real reason (not labelled pr-performance, reused from cache, no integration-test updates, not labelled ci-toolchain, not affected by the changed files, no failed tests from previous runs, no src/Coordination changes).

The only CIDB FAIL rows at this SHA are Config Workflow / Pre Hooks and Finish Workflow / Post Hooks, on checks whose GitHub conclusion is success - bookkeeping rows, not test failures.

Check / test Reason Owner / fixing PR
(none) - -

Session id: cron:our-pr-ci-monitor:20260815-070000

@clickhouse-gh

clickhouse-gh Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

📊 Cloud Performance Report

✅ AI verdict: no_change — no significant changes across 34 queries analysed

This PR is a correctness fix: it snapshots the CAST FunctionConvertSettings once while the constructing context is alive (so casts in stored defaults and mutation expressions no longer read an expired context), and drops WithContext from the resolver. It moves only where those settings are built at plan/build time and changes nothing in the per-row execution path. Because no query hot path is touched, the broad 6-26% ClickBench improvements and the large TPC-H Q7 (×8.6 faster) and Q8 (×2.2 faster) readings cannot be attributed to this change, so all flagged improvements are downgraded to not-sure and read as run-to-run variance (note the small source sample and the very noisy source measurements on ClickBench Q23).

clickbench

⚠️ 11 inconclusive

Flagged queries (11 of 43)
Query Verdict Baseline median (ms) PR median (ms) Change q-value Hint
⚠️ 13 not_sure 426 334 -21.7% <0.0001 This PR only snapshots CAST conversion settings once to fix an expired-context bug; it changes nothing in per-row execution, so a 21.7% shift here is run-to-run variance.
⚠️ 14 not_sure 362 314 -13.3% <0.0001 CAST-settings-snapshot refactor has no per-row execution effect, so this 13.3% delta is not attributable to the PR and reads as measurement variance.
⚠️ 15 not_sure 193 157 -18.7% <0.0001 The change only moves where CAST settings are constructed at build time; it cannot alter row-loop cost, so this 18.7% improvement is run-to-run variance.
⚠️ 16 not_sure 793 592 -25.3% <0.0001 A CAST settings-snapshot correctness fix does not touch execution hot paths, so this 25.3% improvement is not plausibly caused by the PR.
⚠️ 17 not_sure 556 439 -21.0% <0.0001 The diff is a build-time plumbing change to CAST resolution; it changes no per-row work, so this 21.0% delta is environment/run variance.
⚠️ 18 not_sure 1298 1053 -18.9% <0.0001 CAST settings are now snapshotted once instead of per-build, with no effect on execution; this 18.9% improvement is not attributable to the PR.
⚠️ 23 not_sure 167 77 ×2.2 faster <0.0001 A ×2.2 faster reading on a settings-snapshot-only refactor is implausible, and the source measurements here were very noisy; treat as run-to-run variance.
⚠️ 30 not_sure 279 222 -20.4% <0.0001 The PR changes only where CAST conversion settings are built, not how rows are processed, so this 20.4% delta is run-to-run variance.
⚠️ 31 not_sure 395 292 -26.1% <0.0001 No per-row execution changed in this CAST-resolver refactor, so this 26.1% improvement is not plausibly a PR effect.
⚠️ 33 not_sure 1254 1174 -6.4% <0.0001 Build-time-only CAST settings change cannot move a heavy query's row loop; this 6.4% delta is measurement variance.
⚠️ 34 not_sure 1256 1158 -7.8% <0.0001 Same as Q33: a CAST settings-snapshot refactor leaves execution unchanged, so this 7.8% improvement is run-to-run variance.

Change = percent below ×2; the ratio of medians (×N faster/slower) beyond, where percent understates the scale. q-value = BH-FDR adjusted p; smaller is stronger evidence. MIRAI flags a query when q < fdr_q (default 0.10) — the value the verdict is based on.

tpch_adapted_1_official

⚠️ 2 inconclusive

Flagged queries (2 of 22)
Query Verdict Baseline median (ms) PR median (ms) Change q-value Hint
⚠️ 7 not_sure 574 67 ×8.6 faster <0.0001 A ×8.6 faster result cannot come from a build-time CAST settings-snapshot fix with no row-loop change; this is an environment/run-variance artifact.
⚠️ 8 not_sure 175 78 ×2.2 faster <0.0001 ×2.2 faster is implausible for a CAST-resolver plumbing change that alters no per-row execution; treat as run-to-run variance.

Change = percent below ×2; the ratio of medians (×N faster/slower) beyond, where percent understates the scale. q-value = BH-FDR adjusted p; smaller is stronger evidence. MIRAI flags a query when q < fdr_q (default 0.10) — the value the verdict is based on.

Debug info
  • StressHouse run: de98030e-62c8-400d-955c-ef1935f143e7
  • MIRAI run: 2db1a2da-dfe4-4e6e-b073-aff07ec4cb51
  • PR check IDs:
    • clickbench_1797328_1787003703
    • clickbench_1797340_1787003703
    • clickbench_1797346_1787003703
    • tpch_adapted_1_official_1797359_1787003703
    • tpch_adapted_1_official_1797385_1787003703
    • tpch_adapted_1_official_1797399_1787003704

@CurtizJ CurtizJ mentioned this pull request Aug 17, 2026
1 task
@Avogar Avogar self-assigned this Aug 17, 2026
@Avogar
Avogar enabled auto-merge August 17, 2026 17:37
@Avogar
Avogar disabled auto-merge August 17, 2026 17:43
@Avogar

Avogar commented Aug 17, 2026

Copy link
Copy Markdown
Member

@groeneai merge master into this branch, build and check if the test is passed, because in master this now returns differnet result (and that's why sync in private is failed - test failes):

CREATE TABLE t_cast_expired_overflow (d Date, dt DateTime('UTC') MATERIALIZED toDateTimeOrDefault(d, 'UTC')) ENGINE = MergeTree ORDER BY tuple();
INSERT INTO t_cast_expired_overflow (d) VALUES ('2149-06-07'), ('2020-01-01');
SELECT d, dt FROM t_cast_expired_overflow ORDER BY d;

   ┌──────────d─┬──────────────────dt─┐
1. │ 2020-01-01 │ 2020-01-01 00:00:00 │
2. │ 2149-06-06 │ 2106-02-06 00:00:00 │
   └────────────┴─────────────────────┘

The `-- MATERIALIZED default whose cast overflows` arm took its expected
value from `toDateTimeOrDefault`, so it asserted that a `to<Type>OrDefault`
function honours `date_time_overflow_behavior`. That only holds while
ClickHouse#109946 is in the tree. It is reverted on master by ClickHouse#114911, where
`FunctionCastOrDefault` goes back to `castColumnAccurateOrNull` with a null
context and therefore always saturates. The arm passes on this pull
request's base, which predates the revert, and fails against master:

  2149-06-06  2013-04-29 17:31:44   expected, wrap-around
  2149-06-06  2106-02-06 00:00:00   actual, saturating

That is why the private sync reported this test as the only new failure,
in every stateless job it ran in, while the public checks stayed green.

Spelling the conversion as an explicit `CAST` pins the arm to
`CastOverloadResolverImpl`, which is what this change is about, and makes
it read the same before and after the revert. Verified against three
binaries - master `d59f2be34ed3` with the revert in effect, this pull
request's head, and the private sync build - all of which reproduce the
reference byte for byte. The reference file is unchanged.

Related: ClickHouse#109946
Related: ClickHouse#114911
Related: ClickHouse#114912

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@clickhouse-gh clickhouse-gh Bot added the manual approve Manual approve required to run CI label Aug 17, 2026
@groeneai

Copy link
Copy Markdown
Collaborator Author

Confirmed, and it is master's behaviour rather than anything this PR does. I merged master (d59f2be34ed3), rebuilt, and ran your query on the merged branch and on a pristine master build: both print 2149-06-06 2106-02-06 00:00:00, and the full 04527 output is byte-identical between the two binaries.

The route changed under the arm. A stored MATERIALIZED default reaches castColumnAccurateOrNull -> createInternalCast(..., nullptr), which hardcodes Saturate; the old expected 2013-04-29 17:31:44 is the Ignore/wrap-around value. #109946 had made FunctionCastOrDefault build a resolver at execute time, which routed it through buildImpl (Ignore); the #114911 revert put it back on the saturating route. This PR's base sits between the two, which is why the reference carried the Ignore value.

I see @alexey-milovidov has already pushed 0b50647 while I was verifying, spelling the arm as an explicit CAST. That is the better fix and it supersedes what I had staged (I had only updated the reference, which would have broken again on #114912). I verified his head independently: with the explicit CAST both a pristine master build and my merged fix build print 2013-04-29 17:31:44, matching the unchanged reference byte for byte, so the arm no longer swings with the revert. Nothing further from me on the test.

One thing worth flagging, since it changes the merge order. #114912 re-lands #109946 and does not carry this PR's fix: at its head CastOverloadResolver.cpp still has private WithContext and getContext() in buildImpl, and castOrDefault.cpp restores cast_or_null_resolver->build(cast_args) at execute time. That is the caller behind the 31 Context has expired errors CIDB recorded between 2026-08-13 22:45Z and 2026-08-15 05:01Z (zero since, and each of the four hits stamped after the revert is on a base that predates it). So #114912 landing without this PR would re-introduce the exception.

@groeneai

Copy link
Copy Markdown
Collaborator Author

CI finish ledger - 0b50647

Every failure below has an owner: a fixing PR (ours or external), or a full-effort fix task
whose fixing-PR link will be posted here when it opens. Only CH Inc sync is exempt.

Check / test Reason Owner / fixing PR
Finish Workflow / new_tests_check.py PR-caused: no per-arch Bugfix validation validated the bug. Both functional arms ran (amd64 1030 s, aarch64 860 s) and both report Bug does not reproduce on this arch, so 04527 passes against the master-HEAD binary and does not currently act as a regression test for the fix. PR-caused, owned by this PR
CH Inc sync private sync, tests failed (1 new, 1 known) CH Inc sync (private, not actionable by us)

CI is otherwise clean on this head: 179 check-runs, 0 incomplete, 160 success, 18 skipped,
and Config Workflow = success (a real run, not a void one).

Context, since the gate result is expected rather than surprising: 04527's overflow arm was
respelled as MATERIALIZED CAST(d AS DateTime('UTC')) in 0b50647b, which pins it to
CastOverloadResolverImpl and makes it read the same before and after the #114911 revert.
That is what the reference file needed, and it is also why the arm cannot redden on current
master: after #114911 nothing on master keeps a resolver alive past its context, so the
Context has expired path this change fixes is unreachable there. The exception returns when
#114912 re-lands #109946, which is why I flagged that this change should land first or
together with it.

The merge-blocking decision sits in new_tests_check.py, which requires at least one per-arch
strict success for a PR carrying pr-bugfix, so this is a category and sequencing question
rather than something to silence by weakening the test. I am not touching the branch: the head
commit is @ alexey-milovidov's.

Session id: cron:our-pr-ci-monitor:20260818-013000

@groeneai

Copy link
Copy Markdown
Collaborator Author

The blocking new_tests_check.py is not something a push on this branch can clear. Both per-arch functional arms genuinely ran (1029s amd64, 860s aarch64) and report Bug does not reproduce on this arch, and SKIPPED is deliberately not a pass. The reason is #114911: it reverted #109946 out of master, and that removed the only caller which retains a CastOverloadResolverImpl past its context, so on master HEAD there is nothing left to reproduce. Merging master in would not change that, since the revert is exactly what makes master green.

One measurement that I think decides the category question, because it is not visible from master: the defect is live on the 26.7 branch. #114689 backported #109946 there on 08-13 and no revert followed, so the branch still carries both the retaining caller and getContext() in buildImpl. Official branch builds:

26.7.4.53   CREATE TABLE t (s String, ip IPv6 MATERIALIZED toIPv6OrDefault(s)) ENGINE = MergeTree ORDER BY tuple();
            INSERT INTO t (s) VALUES ('::1'), ('not an ip');
            -> Code: 49. DB::Exception: Context has expired: while executing function toIPv6OrDefault. (LOGICAL_ERROR)

26.6.3.55   -> ::1  ::1 / not an ip  ::

The INSERT is rejected outright. The last tagged release, v26.7.3.19-stable, predates the backport, so this is not in a tagged build yet but is queued for the next 26.7 patch.

So Bug Fix looks correct rather than hardening, and this wants a v26.7-must-backport label. This patch applies to 26.7 cleanly, same six files.

I have not pushed anything. The master merge remains your call.

@Avogar
Avogar added this pull request to the merge queue Aug 18, 2026
Merged via the queue into ClickHouse:master with commit 91e2361 Aug 18, 2026
181 of 183 checks passed
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-synced-to-cloud The PR is synced to the cloud repo label Aug 18, 2026
@groeneai groeneai added the groeneai-origin-ci-master PR origin: master/nightly CI monitoring finding label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors groeneai-origin-ci-master PR origin: master/nightly CI monitoring finding manual approve Manual approve required to run CI pr-bugfix Pull request with bugfix, not backported by default pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants