[FLINK-40170][table-planner] Infer update-producing changelog mode for early-fire interval join - #28877
Conversation
…fire interval join With the EARLY_FIRE hint, an outer interval join speculatively emits a padded unmatched row after the delay and corrects it when a match later arrives, so it no longer produces insert-only changes. Teach FlinkChangelogModeInferenceProgram to reflect this. Split StreamPhysicalIntervalJoin into its own ModifyKindSet arm: its children still consume insert-only, but the node provides INSERT and, when the hint makes it update-producing, UPDATE. A new produceEarlyFireUpdates accessor gates that on the hint being set, the join being outer, and a non-negative window span, so the hint stays inert for inner joins and negative-window joins (which only ever emit inserts). The interval join keeps its place in the UpdateKind and DeleteKind arms. When such a join feeds an insert-only downstream, planning fails with a tailored error that names the hint, rather than the generic "doesn't support consuming update changes" message. Runtime behavior is unchanged; the operator still ignores the hint. The compiled-plan round-trip test's sink now declares sink-insert-only=false: its early-fire outer join produces updates under the new inference, so the previously insert-only sink no longer accepts the pipeline.
8f7448f to
4f6ef24
Compare
|
Hi @RocMarshal, #28827 is merged, so this one is rebased onto master and out of draft. The diff is standalone now: 5 files, adding update-producing changelog inference for an early-fire outer interval join. One thing worth flagging: this changes an existing test's sink. PTAL when you have a moment. Thanks! |
There was a problem hiding this comment.
Pull request overview
This PR updates the Flink Table planner’s changelog-mode inference for StreamPhysicalIntervalJoin to account for EARLY_FIRE: outer interval joins with a non-negative window become update-producing, and insert-only downstream consumers are rejected with a hint-specific error. It also updates planner tests and compiled-plan fixtures to reflect the new inferred changelog mode.
Changes:
- Infer
UPDATE(in addition toINSERT) as produced changelog for early-fire outer interval joins with non-negative windows, while still requiring insert-only inputs. - Add a tailored
TableExceptionwhen an early-fire update-producing interval join feeds an insert-only downstream. - Extend/update test coverage and compiled-plan round-trip fixtures to validate changelog inference and sink compatibility.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkChangelogModeInferenceProgram.scala | Adds dedicated StreamPhysicalIntervalJoin handling to infer update-producing modify kinds under EARLY_FIRE and throw a tailored insert-only error. |
| flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/nodes/physical/stream/StreamPhysicalIntervalJoin.scala | Introduces produceEarlyFireUpdates to detect when EARLY_FIRE makes the join update-producing (outer join + non-negative window). |
| flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/hints/stream/EarlyFireJoinHintTest.java | Adds tests asserting inferred changelog modes and the insert-only downstream failure behavior. |
| flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/hints/stream/EarlyFireJoinHintTest.xml | Adds expected optimized-plan outputs for new test cases, including changelog modes for inner/negative-window/outer joins. |
| flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/hints/stream/EarlyFireJoinHintTest_jsonplan/testEarlyFireJsonPlanRoundTrip.out | Updates JSON plan fixture to reflect non-insert-only sink configuration and updated inferred input changelog mode. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (intervalJoin.produceEarlyFireUpdates && !providedTrait.satisfies(requiredTrait)) { | ||
| throw new TableException( | ||
| s"$requester is insert-only, but the EARLY_FIRE hint makes this outer interval join " + | ||
| "produce update changes (a padded row is emitted speculatively and later corrected " + | ||
| "on a match). Remove the EARLY_FIRE hint, or write into a downstream/sink that " + | ||
| "accepts update changes.") | ||
| } |
There was a problem hiding this comment.
Reworded. Went with "doesn't support consuming update changes" to match the existing phrasing in this file (line 609) rather than "requires insert-only", so it reads like the other messages while still naming the hint.
| def produceEarlyFireUpdates: Boolean = | ||
| earlyFireDelay != null && getJoinType.isOuterJoin && | ||
| (windowBounds.getLeftUpperBound - windowBounds.getLeftLowerBound) >= 0 |
There was a problem hiding this comment.
Changed to upper >= lower.
… window bounds directly The insert-only error named the requester as being insert-only, but the requester is the consumer that requires insert-only changes. Use the "doesn't support consuming" phrasing already used elsewhere in this file. Compare the window bounds directly instead of testing the sign of their difference, which cannot overflow and states the non-negative-window condition more plainly.
|
Copilot's two comments are resolved in 5e425a9: the insert-only error now uses the same "doesn't support consuming ... changes" wording as the rest of the file, and the window-span check compares the bounds directly instead of subtracting them. cc @RocMarshal, this one is ready for review when you have time. Thanks! |
RocMarshal
left a comment
There was a problem hiding this comment.
LGTM +1
Given the lack of further responses over the past days,
we will proceed with merging this PR to keep things moving. If there are additional suggestions, please feel free to submit a hotfix or follow-up PR.
Thanks!
|
Thanks @weiqingy for the hard work. |
Part of the FLIP-497 implementation stack under umbrella FLINK-36953. Landing order:
targetoption (#28827, merged)What is the purpose of the change
With the
EARLY_FIREhint, an outer interval join emits a speculative null-padded row after the delay and corrects it when a match later arrives, so its result is no longer insert-only. This makes the planner infer that update-producing changelog mode, and reject an insert-only downstream with an error that names the hint instead of the generic "doesn't support consuming update changes" message. Inner joins and negative-window joins only ever emit inserts, so the hint stays inert for them and they remain append-only.Runtime behavior is unchanged. The operator still ignores the hint; emission lands in PR-4.
Brief change log
FlinkChangelogModeInferenceProgramgets its ownModifyKindSetarm forStreamPhysicalIntervalJoin: children still consume insert-only, but the node providesINSERTand, when the hint makes it update-producing,UPDATE. It keeps its place in theUpdateKindandDeleteKindarms.StreamPhysicalIntervalJoin.produceEarlyFireUpdatesgates that on three conditions: the hint is set, the join is outer, and the window span is non-negative.TableExceptionwhen such a join feeds an insert-only downstream.sink-insert-only=false. Its early-fire outer join produces updates under the new inference, so the previously insert-only sink no longer accepts that pipeline. This is the one existing behavior this PR changes.Verifying this change
This change added tests and can be verified as follows:
EarlyFireJoinHintTest: an early-fire outer join is inferred aschangelogMode=[I,UA]; an insert-only downstream fails with the tailored error; a negative-window join and an inner join both stay[I]while still carryingearlyFireDelayon the join node, so the hint is attached but inert rather than absent.Does this pull request potentially affect one of the following parts:
@Public(Evolving): noDocumentation
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Anthropic)