[FLINK-40528][table-planner] Don't evaluate non-key expressions for partial delete StreamExecCalc - #29106
Open
twalthr wants to merge 1 commit into
Open
[FLINK-40528][table-planner] Don't evaluate non-key expressions for partial delete StreamExecCalc#29106twalthr wants to merge 1 commit into
twalthr wants to merge 1 commit into
Conversation
…artial delete StreamExecCalc
Collaborator
snuyanzin
reviewed
Sep 4, 2026
| }, | ||
| "abilities" : [ { | ||
| "type" : "FilterPushDown", | ||
| "predicates" : [ ] |
Contributor
There was a problem hiding this comment.
probably should be handled separately
snuyanzin
reviewed
Sep 4, 2026
| } ], | ||
| "type" : "BOOLEAN NOT NULL" | ||
| }, | ||
| "partialDeleteKeys" : [ 0 ], |
Contributor
There was a problem hiding this comment.
will it be in plan in case of empty?
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.
What is the purpose of the change
FLINK-37475 (#26306) lets the planner drop
ChangelogNormalizewhen a source only produces partial "delete-by-key" tombstones (key columnsset, all other columns
null/absent) and the sink accepts thesame, avoiding the cost of materializing full row state for upsert-style (e.g. upsert-Kafka)
pipelines.
The planner's
FlinkChangelogModeInferenceProgramdecided whether aCalccould keep forwardingDELETE_BY_KEYpurely by checking whether its filter condition only touches key columns — itnever checked whether the projection does. As a result, a
Calcwith no filter (or akey-safe one) was always allowed to forward
DELETE_BY_KEYunchanged, even though its projectioncould evaluate expressions over non-key columns that are legitimately
null/absent in adelete-by-key row. This could throw at runtime (e.g. constructing
ROW(id, arr)wherearris aNOT NULL ARRAYcolumn that is actuallynullfor the tombstone) or silently produce wrongoutput (e.g.
value + 2evaluated on anullvalue).This PR fixes the runtime side for the common case (
StreamExecCalc, a plain SQL projection):when a
-Drow enters aCalcthat is forwardingDELETE_BY_KEY, only the output columns thatare a trivial pass-through of one of the
Calc's own upsert keys are evaluated; every othercolumn becomes a typed
NULLinstead of evaluating its (potentially unsafe) expression.Python calc and (regular) async calc share the exact same planner-level treatment as plain
Calc(they extend the sameStreamPhysicalCalcBase), but their exec nodes(
CommonExecPythonCalc/CommonExecAsyncCalc) invoke an external/remote function per row and donot get this runtime protection. For these, the planner is instead made conservative: it now
never forwards
DELETE_BY_KEYthrough anyCalcvariant other than the plain one, forcing aChangelogNormalizeto stay upstream so a remote/external call is never made with a partial,possibly-null row.
Brief change log
StreamExecCalc/CommonExecCalc/BatchExecCalcgain an optionalint[] partialDeleteKeysfield (only ever set on
StreamExecCalc, persisted in the JSON plan only when non-null),naming the output column indices that are safe to evaluate on a delete-by-key row.
StreamPhysicalCalccomputespartialDeleteKeysfrom theCalc's own resolvedDeleteKindTraitandFlinkRelMetadataQuery.getUpsertKeys, taking the union of allcandidate upsert-key sets (a
Calc's output can have more than one, e.g. a duplicated/aliasedkey column) so a real key column is never dropped in favor of an unrelated one.
CalcCodeGeneratorgenerates a runtime branch: for a-Drow, only the key-derived outputcolumns are evaluated (via
GenerateUtils.generateNullLiteral. Each branch is generated inside its own pushed local-ref scopeso a cached sub-expression (e.g. the
BinaryRowWritercode backing aROW(...)constructor)is never hoisted into the unconditional/bottom scope and run for every row regardless of
branch.
FlinkChangelogModeInferenceProgram: only the plainStreamPhysicalCalckeeps the existing"forward
DELETE_BY_KEYif the filter is key-safe" check; every otherStreamPhysicalCalcBasesubclass (Python calc, async calc, and any future subclass) now always requires
FULL_DELETE.TableTestPrograms inDeletesByKeyPrograms/DeletesByKeySemanticTests(non-key row-constructor expression, a key-safe filter combined with it, a duplicated/cast
key column, and an async calc regression) and
CalcTestPrograms/CalcRestoreTest(acompiled-plan restore test asserting
partialDeleteKeysround-trips through the plan JSON).Verifying this change
This change added tests and can be verified as follows:
DeletesByKeySemanticTests: coversplain delete-by-key passthrough, a full-delete fallback, a non-key projection expression, the
same combined with a key-safe filter, a
NOT NULL ARRAYwrapped in aROW(...)constructor,a duplicated/cast key column producing multiple upsert-key candidates, and an async calc
(verifying the planner now forces a full, materialized delete instead of letting the
delete-by-key tombstone reach the remote function).
CalcRestoreTest#calc-partial-delete-with-expression-and-filter: a new compiled-plan restoretest where the delete-by-key row is only produced after restoring from the compiled plan
JSON, verifying
partialDeleteKeysround-trips correctly through plan serialization.CalcITCase,CalcRestoreTest,CalcBatchRestoreTest,AsyncCalcRestoreTest,PythonCalcJsonPlanTest,PythonAsyncCalcJsonPlanTest,ChangelogNormalizeOptimizationTest,FlinkCalcMergeRuleTest,PushCalcPastChangelogNormalizeRuleTest, andCalcMergeTest— confirms no existing goldenplan or behavior changed for any Calc node that isn't in a delete-by-key pipeline.
corresponding fix and confirming the associated test fails (a crash or wrong/partial output
without the fix, the expected result with it).
Does this pull request potentially affect one of the following parts:
@Public(Evolving): no (allchanged classes are internal planner/exec-node classes)
CalcCodeGeneratoris onthe hot path for every
Calc; the change adds oneOptionmatch per generated operator withno extra generated code when
partialDeleteKeysis unset (the overwhelming majority ofCalcnodes), and only branches at runtime forCalcs that are already in a delete-by-keypipeline.
Kubernetes/Yarn, ZooKeeper: no
Documentation
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Sonnet 5