Skip to content

Fix cast initialization, day-time interval output, and FORCE join validation - #30

Closed
Flanderzz wants to merge 3 commits into
datafusion-contrib:mainfrom
Flanderzz:fix/engine-correctness
Closed

Flanderzz wants to merge 3 commits into
datafusion-contrib:mainfrom
Flanderzz:fix/engine-correctness

Conversation

@Flanderzz

@Flanderzz Flanderzz commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Summary:

Found three issues in the shared code on main while validating the Flink 2.1.3 backport: cast initialization uses an invalid null input, day-time interval expressions cannot safely pass through the native pipeline, and join substitution can hide an error Flink is supposed to raise. These fixes belong on main independently of 2.1 support. They need to land before the backport can build on them; the version-specific changes remain in a separate PR.

Changes:

  • Warms generated casts with valid values for non-nullable inputs instead of null, without swallowing initialization errors.
  • Admits day-time interval projections and converts their output to integer milliseconds before downstream native operators consume it. On current main, the output-type check otherwise rejects this shape.
  • Checks filter and join predicates at planning time so unsupported interval comparisons fall back rather than fail during execution.
  • Preserves Flink's rejection under table.optimizer.delta-join.strategy=FORCE. The guard is conservative per optimizer block; it does not add native delta-join support.
  • Adds regression tests and updates coverage documentation. Duration and year-month interval gaps remain unchanged.

Testing

Validated locally:

  • Full runtime suite: mvn -B -ntp -pl :streamfusion-runtime test1,074 tests, 0 failures/errors, 33 skipped.
  • Native library: cargo test --manifest-path native/Cargo.toml --lib400 passed.
  • SQL parity against stock Flink, including nullable interval keys and join predicates. The new interval-key, filter, and join-residual cases reproduced runtime failures before their fixes.
  • Earlier backport validation ran Flink's full planner-runtime integration suite on 2.1.3 and 2.2.1 with no failures.

@Flanderzz
Flanderzz marked this pull request as draft September 6, 2026 07:36
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.
Flink carries day-time intervals as integer milliseconds. DataFusion compiles
interval literals as Arrow day/millisecond pairs instead, which the existing
output-type check rejected and downstream keyed consumers could not encode.

Accept that inferred type only for a top-level SQL day-time interval, never
for BIGINT. Convert Calc's materialized interval columns to nullable integer
milliseconds before they reach exchanges or keyed operators, while retaining
the original inferred type for the planner's SQL-type check. The host reader
also accepts Arrow's day-time encoding. Nested fields retain their existing
checks, without positional pairing of Flink and Arrow child layouts.

Apply the same plan-time compilation check to filter-only Calcs and join
residual predicates. Otherwise a
separate filter can encode a comparison between a materialized interval and
an interval literal yet fail to coerce it on its first batch. Unsupported
comparisons now fall back with their compilation reason.

Timestamp differences producing Duration remain on the host: Flink subtracts
the operands' millisecond values, not a narrowed nanosecond difference.
Year-month literals and Arrow calendar-month interval output are not added.
Boundary diagnostics name the Arrow vector and SQL type when a reader or
writer cannot handle a representation.
Flink validates a forced delta-join strategy after our substitution pass.
Removing an ordinary join can erase the evidence that makes Flink reject a
statement, allowing a query that the host intentionally refuses.

Leave an optimizer block unchanged under FORCE when it contains an ordinary
join and no delta join. A delta join in the same block bypasses this guard
only: it remains unsupported natively, and ordinary admission and island
checks still apply.

The host validates across all statement roots, while this pass sees one
optimizer block at a time. A regular-join block therefore still falls back
when the only delta join is in another block, even if the host accepts the
statement. Record this conservative scope in the fallback reason and coverage
docs. Test actual regular and delta rel nodes through the guard, rather than
a separate boolean policy that bypasses traversal.
@Flanderzz
Flanderzz force-pushed the fix/engine-correctness branch from 57213a7 to aea3e04 Compare September 7, 2026 03:11
@Flanderzz Flanderzz changed the title Correct a cast warm-up, an interval boundary, and a forced-join substitution Fix cast initialization, day-time interval output, and FORCE join validation Sep 7, 2026
@Flanderzz
Flanderzz marked this pull request as ready for review September 7, 2026 03:12

@jordepic jordepic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracking these down. All three fixes are real: I reproduced both crash premises against main and confirmed the branch resolves them, ran the PR's test classes (103/103), the two native tests plus cargo fmt --check, and added a dozen parity probes of my own. Requesting changes for one correctness gap in the interval work and one design point; the rest is polish.

Confirmed

  • Cast warm-upSELECT CAST(COALESCE(s, '0') AS INT) FROM t fails the job on main at operator open (NullPointerException: BinaryStringData.trim() wrapped in Error when casting STRING NOT NULL to INT NOT NULL). Flink's AbstractNullAwareCodeGeneratorCastRule emits no null guard for a NOT NULL input, so the diagnosis is exactly right. Native parity on the branch.
  • Interval filterWHERE dur > INTERVAL '1' HOUR on a source interval column panics natively on main (failed to coerce predicate … has type Int64 … has type Interval). The branch falls back at plan time with that reason.
  • FORCE guard — mirrors StreamPhysicalDeltaJoinForceValidator's finder, conservative per optimizer block, documented, and tested with real rel nodes.

Blocking: interval arithmetic followed by a comparison is wrong natively

arrow-rs IntervalDayTime derives Ord lexicographically on (days, milliseconds) and = compares the two components, so a non-normalized pair produced by +/- compares incorrectly. With t(k BIGINT), k = 1..4:

Query Flink Native
SELECT k FROM t WHERE (CASE WHEN k > 2 THEN INTERVAL '1' DAY ELSE INTERVAL '1' HOUR END) + INTERVAL -'2' HOUR < INTERVAL '23' HOUR 1, 2, 3, 4 1, 2
same with = INTERVAL '22' HOUR 3, 4 (none)

Native holds (1 day, −7 200 000 ms) and compares it against (0, 82 800 000) component-wise. This predates the PR (the same queries diverge on main), but this PR is the one codifying interval handling in filter-only Calcs and documenting it, and projecting the same expression is correct only because Calc normalizes afterwards. Either normalize before the comparison, or have the encoder decline comparison and arithmetic between two interval operands (keeping timestamp ± interval).

Design: one representation, decided once

The batch is converted in calc.rs, but inference still reports Interval(DAY_TIME), so Java grows a readsAs exemption plus ArrowIntervalDayColumnVector — and no runtime path can reach that vector (Calc converts, nothing else emits an interval array). The days * 86_400_000 + ms conversion now lives in three places (calc.rs, over_agg.rs::normalize_interval_columns, expr.rs::IntervalScale), and the OVER helper is now dead code with a comment that describes behaviour Calc no longer has.

Doing it once in the expression compiler — wrap each interval-typed projection root and each interval comparison operand in the to-millis conversion IntervalScale already contains — fixes the blocking item, makes infer_calc_output_schema agree with evaluate by construction, and lets you delete the Java vector, the readsAs exemption, the per-batch downcast probe, and the OVER helper. The stated reason for keeping inference on Interval (guarding a BIGINT-declared projection) doesn't hold: nothing the encoder emits can yield an interval array for a Calcite BIGINT expression — interval literals and CASEs over them are INTERVAL-typed, and IntervalScale already returns Int64.

Docs

The residual-predicate paragraph is pasted verbatim onto four join pages, and the FORCE guard is explained three times (index.md, regular-join.md, unsupported.md) with overlapping sentences. Repo convention is one explanation under Global switches on docs/operators/index.md and a one-line bullet linking to it from each page. The calc-filter.md addition narrates implementation ("Inference retains the expression's interval type so this exemption cannot admit…") rather than stating what is admitted; the coverage page should just say day-time interval expressions are native, timestamp differences and year-month intervals fall back, and why.

Nits

  • FilterCalcMatcher.matches now encodes the Calc twice (encodedCondition then encodeCalc).
  • The BOOLEAN arm in warmupValue is unreachable — hostCastSupported never admits a boolean source.
  • The three identical "Unsupported type %s (Arrow vector %s, arrow type %s)" blocks in ArrowConversion want a helper.
  • Commit 2's message has a stray line break ("Otherwise a\nseparate filter").
  • The branch is 12 commits behind main; it rebases cleanly.

jordepic added a commit that referenced this pull request Sep 16, 2026
Integrate the cast initialization and FORCE-join repairs from PR #30, retaining its authorship in the preceding commits. Current temporal lowering already carries day-time milliseconds and year-month integers through the scalar bridge, so its interval cases need regression coverage rather than the older Arrow interval conversion and conservative fallback implementation.

The ten interval cases now assert actual native execution for signed literals, nullable grouping keys, filters after Top-N, outer-join residuals, nanosecond timestamp differences, and year-month CASE results. Add the audited runtime CHAR(8) NOT NULL to DOUBLE query. Two focused runs pass 32 and 81 tests against released Flink 2.2.1, including 52 dynamic collection cases and decimal/binary UDF ownership checks.

Fixes #84. Fixes #89. Fixes #90.

Co-authored-by: Devine Chinemere <Fredfres1@gmail.com>
@Flanderzz

Copy link
Copy Markdown
Contributor Author

Closing this cause it seems these changes landed yesterday

603f7c1 carries commit 1 (cast warm-up)
1d86e4c carries commit 3 (FORCE delta-join guard)
adb55b1 replaces commit 2: the current temporal lowering already carries day-time milliseconds through the scalar bridge, so the Arrow interval conversion and conservative fallback aren't needed (nice work)

🎉🎉🎉🎉🎉🎉

@Flanderzz Flanderzz closed this Sep 18, 2026
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.

2 participants