Skip to content

[886] Add Delta Kernel source/target coverage to ITConversionController - #903

Open
vaibhavk1992 wants to merge 5 commits into
apache:mainfrom
vaibhavk1992:issue-886-delta-kernel-default
Open

[886] Add Delta Kernel source/target coverage to ITConversionController#903
vaibhavk1992 wants to merge 5 commits into
apache:mainfrom
vaibhavk1992:issue-886-delta-kernel-default

Conversation

@vaibhavk1992

Copy link
Copy Markdown
Contributor
  • testVariousOperationsDeltaKernelSource runs the same dataset-equivalence checks as testVariousOperations, forcing DeltaKernelConversionSourceProvider across both sync modes and both partitioning cases.
  • testVariousOperationsDeltaKernelTarget does the same for the DELTA target leg, routing through DeltaKernelConversionTarget via DeltaConversionTargetConfig.USE_KERNEL, using ConversionTargetFactory's normal dispatch path.
  • testVariousOperations body extracted into runVariousOperationsTest so both new tests share the same assertions the Standalone paths are validated with; existing Standalone matrix is unchanged.

The target-side test currently fails: DeltaKernelConversionTarget only calls withSchema() on table creation, so schema evolution on an existing table (e.g. a new nested field added inside an array column) never reaches the Delta log's Metadata action. Traced to open upstream gap delta-io/delta#4305. Follow-up fail-fast guard tracked separately.

Important Read

  • Please ensure the GitHub issue is mentioned at the beginning of the PR

What is the purpose of the pull request

(For example: This pull request implements the sync for delta format.)

Brief change log

(for example:)

  • Fixed JSON parsing error when persisting state
  • Added unit tests for schema evolution

Verify this pull request

(Please pick either of the following options)

This pull request is a trivial rework / code cleanup without any test coverage.

(or)

This pull request is already covered by existing tests, such as (please describe tests).

(or)

This change added tests and can be verified as follows:

(example:)

  • Added integration tests for end-to-end.
  • Added TestConversionController to verify the change.
  • Manually verified the change by running a job locally.

- testVariousOperationsDeltaKernelSource runs the same dataset-equivalence
  checks as testVariousOperations, forcing DeltaKernelConversionSourceProvider
  across both sync modes and both partitioning cases.
- testVariousOperationsDeltaKernelTarget does the same for the DELTA target
  leg, routing through DeltaKernelConversionTarget via
  DeltaConversionTargetConfig.USE_KERNEL, using ConversionTargetFactory's
  normal dispatch path.
- testVariousOperations body extracted into runVariousOperationsTest so both
  new tests share the same assertions the Standalone paths are validated
  with; existing Standalone matrix is unchanged.

The target-side test currently fails: DeltaKernelConversionTarget only calls
withSchema() on table creation, so schema evolution on an existing table
(e.g. a new nested field added inside an array column) never reaches the
Delta log's Metadata action. Traced to open upstream gap
delta-io/delta#4305. Follow-up fail-fast guard tracked separately.
DeltaKernelConversionTarget.commitTransaction() only applies withSchema()
on CREATE_TABLE (Delta Kernel 4.0.0 limitation, delta-io/delta#4305), so
schema evolution on an already-existing table was silently dropped: rows
kept syncing, but a newly-added nested field (e.g. inside a struct that is
an array column's element type) never reached the Delta log's registered
schema, making it unreadable even though the underlying Parquet had it.

syncSchema() now compares the incoming schema (as a Kernel StructType, to
avoid any InternalSchema<->StructType round-trip noise) against the
existing snapshot's schema, and throws NotSupportedException as soon as
they differ for an existing table, instead of committing a stale schema.
ConversionController/TableFormatSync catches this per-target and records
it as a SyncStatusCode.ERROR, leaving the target at its last-good state
rather than partially written.

testVariousOperationsDeltaKernelTarget updated accordingly: it no longer
reuses runVariousOperationsTest's full lifecycle (which assumes success),
since the exception surfaces as a SyncResult status rather than a thrown
exception from ConversionController.sync(). It now syncs an initial
snapshot (expected SUCCESS), evolves the schema, syncs again (expected
ERROR with the delta-io/delta#4305 reference), and confirms the target's
row count didn't move.
testCreateSnapshotControlFlow and testTimestampNtz each did two syncSnapshot
calls to the same existing table, with the second call's schema silently
adding a field on top of the first (schema2 = schema1 + float_field). This
is exactly the schema-evolution-on-an-existing-table scenario
DeltaKernelConversionTarget now rejects (see the syncSchema guard added
earlier). Before that guard existed, Kernel silently accepted the second
sync without registering the new schema, and these tests only checked
physical file presence/size, not schema correctness, so they never caught
that the second sync's schema change was being dropped.

Neither test is actually about schema evolution: testCreateSnapshotControlFlow
is about the file add/remove control flow across two sequential snapshot
syncs, and testTimestampNtz is about TIMESTAMP_NTZ handling. Fixed both to
reuse the same schema for their second snapshot, matching their real intent.

Added testSchemaEvolutionOnExistingTableFailsSync as a dedicated test for
the guarded behavior: first sync succeeds, second sync (with an evolved
schema) returns SyncStatusCode.ERROR referencing delta-io/delta#4305, and
the table is left at its last-good state rather than partially written.
@vaibhavk1992
vaibhavk1992 force-pushed the issue-886-delta-kernel-default branch from 85aef09 to 58683ce Compare August 22, 2026 15:46

@slachiewicz slachiewicz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The coverage matches prerequisite 1 of #886: source and target, both sync modes and both partitioning cases. Ran the two new tests locally on JDK 11, ITConversionController#testVariousOperationsDeltaKernelSource+testVariousOperationsDeltaKernelTarget → 8/8 pass.

The only red check is spotless: two comment wraps in DeltaKernelConversionTarget plus the assertThrows and NotSupportedException imports that 58683ce left unused. ./mvnw spotless:apply fixes both, but run it on JDK 17 or older, since google-java-format 1.10.0 refuses newer JVMs. Everything else in that run was green, ITConversionController included.

One gap worth closing: nothing syncs an existing table twice with an unchanged schema through the Kernel target, which is the path the new guard can false-positive on. syncSchema compares cachedSnapshot.getSchema() against schemaExtractor.fromInternalSchema(...), so any asymmetry in the InternalSchema/StructType round trip turns a no-op sync into a SyncStatusCode.ERROR. TestDeltaKernelReadWriteIntegration#testIncrementalUpdates covers this for a simple schema only; a second unchanged-schema sync asserting SUCCESS in testVariousOperationsDeltaKernelTarget's first block would extend it to the partitioned and timestamp cases.

Scope note for #886: this lands prerequisite 1 and the fail-fast, not the default flip. While the target fails fast, making Kernel the default turns every schema-evolving Delta pipeline into an error, so the flip still waits on either the fallback to Standalone or delta-io/delta#4305.

This comment was created with AI assistance.

@vaibhavk1992

Copy link
Copy Markdown
Contributor Author

Thanks for the review @slachiewicz

Spotless: fixed in 07cf223.

Why throw instead of falling back to Standalone: DeltaConversionTarget.init()
unconditionally spins up its own SparkSession. A silent fallback would mean a
Kernel-configured pipeline (chosen specifically to drop the Spark dependency) pulls
Spark back in transparently the moment schema evolves — that undercuts the point of
#886. Throwing is also more debuggable: the failure shows up immediately as
SyncStatusCode.ERROR instead of surfacing later as "why did Standalone write this
commit."

If you need schema evolution today: set xtable.delta.target.use_kernel=false to route
that target through Standalone, which supports it. That's the escape hatch this PR
preserves — the default flip stays out of scope until either delta-io/delta#4305
lands or we deliberately accept the fallback tradeoff above.

…eltaKernelTarget

Per PR apache#903 review feedback: nothing exercised syncing an already-existing
table twice with an unchanged schema through the Kernel target, which is
exactly the path the syncSchema() drift guard could false-positive on if
there's any asymmetry in the InternalSchema<->StructType round trip.

Added a second sync (50 more rows, same schema) to the first block of
testVariousOperationsDeltaKernelTarget, asserting SyncStatusCode.SUCCESS,
before moving on to the schema-evolution block that expects ERROR. Covers
both partitioned and unpartitioned cases via the existing parameterization.
@vaibhavk1992

Copy link
Copy Markdown
Contributor Author

@vinishjail97 did some validation for prerequisite 2.
#711 (column rename → wrong Iceberg field ID): root cause is in IcebergSchemaSync.addUpdates (the sink, matches columns by name not field ID). Both Delta extractors already preserve field IDs correctly on the source side — the bug is downstream and format-agnostic. Fixing it once fixes both paths.
#813 (_delta_log mistaken for a Hudi partition): this is Hudi's own filesystem-fallback listing behavior, triggered by any standards-compliant _delta_log directory. Kernel and Standalone produce identical log layouts, so neither is more or less exposed.
#641 (Iceberg source NPE): crashes entirely on the Iceberg-source side (IcebergColumnStatsConverter), before any Delta target code — Standalone or Kernel — ever runs. Doesn't belong in this list at all.

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