Skip to content

FEAT: support for flink 2.1.3 - #45

Draft
Flanderzz wants to merge 10 commits into
datafusion-contrib:mainfrom
Flanderzz:tenet/backporting-flink-2.1.3
Draft

Flanderzz wants to merge 10 commits into
datafusion-contrib:mainfrom
Flanderzz:tenet/backporting-flink-2.1.3

Conversation

@Flanderzz

Copy link
Copy Markdown
Contributor

Flink's streaming planner is not a stable API, so targeting more than one
release line normally means duplicating the planner integration. Measuring the
actual divergence first showed it is far smaller than that: across everything
StreamFusion links against, the 2.1 and 2.2 planners disagree on a handful of
members, and most of those are renames that erase to the same bytecode. What
genuinely differs is narrow — a watermark push-down argument added in 2.2, two
changelog-normalize predicates that only exist in 2.2, a state-backend accessor
absent from 2.1, and one helper whose Scala and Java collection return types are
indistinguishable after erasure and so only surface when compiling.

Concentrate that divergence in a single class compiled once per line and
selected by a build profile, with the values it hands back carried in
line-neutral holders. The rest of the tree compiles unchanged for either target,
which keeps the seam reviewable and stops version drift from leaking into the
matchers and execution nodes.

The profile also pins the dependencies that track the Flink line rather than our
own release cadence: the Kafka connector and the Delta connector publish a build
per line, and Flink's Protobuf format generates against a different Protobuf
runtime major on each, which fails at runtime inside Flink's own deserializer
rather than at build time.

Flink's streaming planner is not a stable API, so targeting more than one
release line normally means duplicating the planner integration. Measuring the
actual divergence first showed it is far smaller than that: across everything
StreamFusion links against, the 2.1 and 2.2 planners disagree on a handful of
members, and most of those are renames that erase to the same bytecode. What
genuinely differs is narrow — a watermark push-down argument added in 2.2, two
changelog-normalize predicates that only exist in 2.2, a state-backend accessor
absent from 2.1, and one helper whose Scala and Java collection return types are
indistinguishable after erasure and so only surface when compiling.

Concentrate that divergence in a single class compiled once per line and
selected by a build profile, with the values it hands back carried in
line-neutral holders. The rest of the tree compiles unchanged for either target,
which keeps the seam reviewable and stops version drift from leaking into the
matchers and execution nodes.

The profile also pins the dependencies that track the Flink line rather than our
own release cadence: the Kafka connector and the Delta connector publish a build
per line, and Flink's Protobuf format generates against a different Protobuf
runtime major on each, which fails at runtime inside Flink's own deserializer
rather than at build time.
The loader shadows a private Flink class, so it has to match that class's exact
signatures — and the two supported lines disagree on one. The accessor exposing
the component classloader was narrowed to a concrete type in 2.2, so each line's
callers are compiled against a different descriptor. Declaring either type alone
turns a batch adaptive-join path into a missing-method failure on the other line.

Declare the wider type in a base class and narrow it in the shim so the compiler
emits a bridge, leaving one class that answers both descriptors. That keeps the
shadow a signature detail rather than a second copy of the file to maintain.

The fail-closed version whitelist is now compiled per line as well, so a build
admits only the patch releases it was actually validated against instead of
inheriting a list that happens to be true for a different target.
The suite installs StreamFusion by matching Flink classes by name at load time.
A miss has always been silent: the suite runs stock Flink end to end, reports
every test green, and proves nothing. That is tolerable while one Flink release
is targeted and the names are known good, but it becomes actively misleading the
moment the suite runs against more than one line, where a renamed or relocated
injection point is exactly the failure being looked for.

Record whether the injection point was instrumented and whether it was actually
entered, then abort at shutdown if Flink's planner factory was loaded without it.
A run that never installed the engine now fails loudly instead of passing.
The suite harness pinned one release throughout: the tag it cloned, the connector
release it resolved, and the profile StreamFusion itself was built with were all
fixed independently. Running it against another line therefore risked the worst
possible outcome — an engine compiled for one planner exercised against another,
producing either a confusing failure or, worse, a green run that measured a
mismatched pair.

Derive all of it from the requested Flink version instead, so choosing a line
selects the source tag, the matching connector release, and the build profile
together. The harness can now be pointed at any supported line without editing it.
Parity tests assert that a query produces identical results on stock Flink and
on the native plan. That assertion is undefined when Flink itself cannot plan
the query: an older line rejects a left-joined ordinality unnest outright, in
its own optimizer, before the engine is ever installed. The failure looks like a
StreamFusion defect and is not one.

Gate such a case on the Flink release that fixed it, reading the version from the
running Flink rather than a build property so a test can never be excused on a
version it did not actually load. The exclusion is deliberately narrow: it covers
only behavior the host lacks, never a coverage gap of ours, which must keep
failing. Newer lines continue to run the case with nothing skipped.
Casts are evaluated through Flink's own cast rules, and the code
generated for a non-nullable input dereferences the value with no null
guard. Priming that code with a null therefore aborted operator startup
rather than surfacing the per-row error the query expects, turning every
cast of a non-nullable string into a job failure.

Prime with a value the declared input type admits instead. This was
invisible locally because the harnesses only ever exercised nullable
columns; non-nullable types reach the engine through SQL literals, so
only the upstream suite covered the shape that broke.
Day-time intervals cross the columnar boundary in two shapes. The engine
canonicalises on integer milliseconds, but the query engine emits a
native interval array whenever an expression's result type is an interval
rather than a timestamp. The native side already absorbed both encodings;
the Java boundary did not, so an interval-typed expression failed at
runtime instead of returning a value.

Read the native encoding as milliseconds at the boundary, mirroring how
the TIME type already absorbs several Arrow encodings behind one SQL
type. Boundary errors now name the Arrow vector and its type alongside
the SQL type: dispatch happens on the vector, so the SQL type alone
cannot explain a mismatch, and that blind spot hid this bug and an
earlier one in the same code.
Flink validates a forced delta-join strategy after our pass has run, and
raises an error only when an ordinary join survived optimization.
Substituting that join away erased the very evidence the validation looks
for, so a query the host intends to refuse ran silently instead. Any
acceleration that changes whether a query is legal is a correctness bug,
not a coverage question.

Decline such plans wholesale. The condition mirrors the host's exactly
rather than declining whenever the strategy is forced, so acceleration is
retained for every plan the validation would have passed, including one
that mixes a delta join with joins we do accelerate.
The upstream-suite harness hardcoded a single Calcite version while Flink
pins one per release line, so on the older line the engine was built
against a parser the host never uses and whole suite modes failed before a
single test ran. Take the version from the Flink checkout under test rather
than restating it, and fail loudly if it cannot be found.

The generated classpath is named per version as well, so a reused build can
no longer silently run one line's tests against the other line's jars.
The engine builds against two Flink lines, but every deployable module
published one artifact name for both. Two builds with different bytecode
claimed identical coordinates, so they overwrote each other in a local
repository and only one could ever be published. A deployment had no way
to ask for the line it runs on, and the image build silently paired one
line's base image with the other line's payload.

Name deployables after the line they target, following how the rest of
the Flink ecosystem publishes per-line artifacts. Release, packaging,
install, and image tooling now take the line explicitly rather than
assuming the default, so a bundle holds one line's jars and an image
cannot be assembled from a mismatched pair.

The internal module that compiles the shared tree and hosts the test
suite is no longer published: it was never part of a deployment, and one
unsuffixed name covering two lines' bytecode is the same collision.
@Flanderzz
Flanderzz marked this pull request as draft September 8, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant