Skip to content

fix: preserve native CASE field metadata on the runtime branch - #172

Merged
osipovartem merged 2 commits into
df55-physical-udf-instancefrom
fix/case-native-field-inference
Oct 30, 2026
Merged

osipovartem merged 2 commits into
df55-physical-udf-instancefrom
fix/case-native-field-inference

Conversation

@osipovartem

Copy link
Copy Markdown
Collaborator

Which issue does this PR close?

Backport the applicable CASE field-metadata corrections from Apache DataFusion
#26145 and
#26158.

The base is df55-physical-udf-instance at 78e3e064e, which Rustice actually
pins, not this repository's default branch. This is a fork-specific adaptation,
not a cherry-pick of Apache's different metadata merge policy.

Rationale for this change

Logical and physical CASE inference previously dropped function result metadata
past depth 8. The logical fallback also discarded metadata for native expressions
outside its allowlist. This loses known type annotations without reporting an
unsupported expression or an error.

Strict workspace Clippy additionally exposed missing delegation of this fork's
CASE literal-matcher hook through ScalarUDF::with_aliases. Aliases must preserve
the function's matcher, fallback and errors, rather than silently disabling its
indexed dispatch. That correction is kept in a separate commit.

What changes are included in this PR?

  • Remove the function-depth cutoff and reuse native cached physical UDF fields.
  • Infer logical CASE, forwarding wrappers and binary fields bottom-up; other
    expressions use native to_field, with no depth classifier or structural rescans.
  • Share the existing native binary coercion and diagnostics, preserving this
    branch's nullable-OR behavior.
  • Preserve the fork's strict metadata merge, annotated-NULL participation,
    type-only cast rules and physical NOT non-inheritance.
  • Retain the first-THEN type result and skip unnecessary metadata-field walks for
    a typed unmarked binary/NOT first result. This fast path is valid for the fork's
    strict merge policy; it is not applicable to Apache's shared-key merge rule.
  • Add exact deep-UDF, aliased NULL CAST/TRY_CAST, NOT, and counted-inference tests.
    Keep existing deep-binary and marked-NULL expectations intact.
  • Delegate the CASE matcher hook through single and nested UDF alias wrappers.
  • Add an ignored, bounded, public-API field-inference benchmark with 13 fixtures
    and exact type/nullability/metadata preflight assertions.

Are these changes tested?

  • cargo fmt --all --check and git diff --check: passed.
  • cargo clippy --all-targets --all-features --locked --offline -- -D warnings:
    passed for the full workspace.
  • Full extended workspace tests: 10,747 Rust tests passed, 0 failed; 9 existing
    ignored tests plus the new ignored benchmark. All 509 SQLLogicTest files passed.
  • Negative controls reproduced the old depth-cutoff metadata loss, unnecessary
    field/type lookups and the aliased matcher loss before their respective fixes.
  • A separate read-only agent reviewed correctness, regressions, performance,
    API compatibility and test coverage. Final source/artifact review explicitly
    approved PR creation and merge, with no blocking findings or outstanding gates.

Runtime-branch performance qualification

The benchmark measures Expr::to_field on prebuilt expressions, not query
execution. Baseline and candidate use identical fixtures, compiler, lockfile,
default features, Arrow 59 and a temporary ci-derived opt-level-1 profile.
The dedicated benchmark profile is cleaned between source roots to prevent
cross-workspace Cargo artifact reuse. Executables are frozen before alternating
fresh-process runs; warmup, validation, formatting and field destruction are
outside the timed region.

Five alternating fresh-process pairs, with 20 warmups and 101 samples per fixture
per process, passed all preflight checks. The table reports the median of five
process medians in microseconds, not SQL latency or a p95:

Fixture Baseline Candidate Change
64 scalar literals 4.356 4.436 +1.8%
1,000 scalar literals 63.877 64.256 +0.6%
64 plain columns 10.713 11.019 +2.9%
64 identically marked columns 19.477 19.577 +0.5%
Shallow CASE/UDF, depth 1 2.723 2.642 -3.0%
Shallow CASE/UDF, depth 4 10.018 10.279 +2.6%
Nested aliases, depth 32 10.387 10.307 -0.8%
Nested aliases, depth 128 38.963 40.298 +3.4%
Nested casts, depth 32 15.293 15.248 -0.3%
Nested casts, depth 128 58.323 58.837 +0.9%
Nested binary CASE, depth 8 6.213 6.104 -1.8%
Nested binary CASE, depth 32 24.094 24.012 -0.3%
Nested binary CASE, depth 128 95.185 95.293 +0.1%

The intermediate approximately 2x nested-binary regression was removed before
final qualification. The remaining measured increases are small planning costs,
not proof of noise or zero regression: depth-128 aliases add 1.335 us, plain
columns add 0.306 us, and the shallow depth-4 UDF control adds 0.261 us.
They are accepted for removing silent annotation loss while preserving the
fork's established semantics and common fast paths. Deep-UDF cases with wrong
baseline metadata are correctness tests, not paired performance claims.
CASE/UDF native re-inference remains a separate limitation.

Environment: Rust 1.97.0, Arrow 59.2.0, WSL2 x86-64, Ryzen 9 9950X3D. Default
datafusion-expr features are identical. Cargo's artifact records confirm
opt-level 1, debug info 0, debug assertions and overflow checks enabled, and a
fresh build from the intended source directory. This is a local planning screen,
not the reserved-host benchmark campaign or release-query qualification.

Identities (SHA-256):

  • Identical benchmark source at both revisions:
    84c956ecf80e17150f2759c06b3533776bfe877ae8bc7ecd3ca2c2dd6d5e6693.
  • Unchanged Cargo.lock:
    b21ee5ea28f8b057ad9dcc4f1ab14458ba3c87da7659f0088cafef41973a371a.
  • Frozen baseline executable:
    bc95c18eca32b794f21925414d0a45c1fea23fe69c90d4ebc2d71356fe47a4e6.
  • Frozen candidate executable:
    4482fbd44dc9e313fc2ca31b58a44ea8495c06a80a8a84a89af05149095477ca.

To reproduce, copy the same benchmark into the base revision and build each
revision separately with the temporary profile below. Clean that profile between
source roots, freeze each printed executable, then alternate five pairs of
<executable> --exact case_field_metadata_perf --ignored --nocapture:

cargo +1.97.0 \
  --config 'profile.ci-case-perf.inherits="ci"' \
  --config 'profile.ci-case-perf.opt-level=1' \
  test --profile ci-case-perf -p datafusion-expr \
  --test case_field_metadata_perf --no-run --locked --offline

Are there any user-facing changes?

Known CASE annotations are preserved instead of silently disappearing after an
arbitrary nesting depth; aliased comparison UDFs retain their literal matcher.
There is no public API, manifest, lockfile or runtime-config change, and no new
per-row conversion or wrapper is added.
This PR does not update Rustice's dependency revision. Arrow 60 upstream timings
and these Arrow 59 planning microbenchmarks are not SQL, Iceberg or SPCS speedups.

@osipovartem
osipovartem merged commit bc0b207 into df55-physical-udf-instance Oct 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant