Skip to content

Flink: Preserve RowKind in DynamicIcebergSink's DataConverter - #18101

Merged
pvary merged 1 commit into
apache:mainfrom
lucasgameiroborges:flink-dynamic-sink-preserve-rowkind
Sep 14, 2026
Merged

pvary merged 1 commit into
apache:mainfrom
lucasgameiroborges:flink-dynamic-sink-preserve-rowkind

Conversation

@lucasgameiroborges

Copy link
Copy Markdown
Contributor

What

RowDataConverter.convert() rebuilds a record whenever its schema is not byte-identical to the target table's — a column-order difference, a missing optional column, a widening type. It builds the replacement as:

GenericRowData targetData = new GenericRowData(fieldGetters.length);

GenericRowData(int) defaults RowKind to INSERT, and nothing downstream restores the original kind — RowKind is never referenced anywhere else in the sink/dynamic package.

Why

BaseDeltaTaskWriter dispatches purely on RowKind. So a CDC delete/UPDATE_BEFORE that happens to go through this conversion path (e.g. a delete envelope carrying only key columns) is treated as an INSERT:

  • against a table with a required non-key column → NullPointerException downstream
  • without one → silently written as an insert of a mostly-null row instead of a delete

Everything else the converter does is already correct — it maps fields by name, null-fills missing optional columns, and widens types — so carrying the kind through is the whole fix.

Fix

GenericRowData targetData = new GenericRowData(sourceData.getRowKind(), fieldGetters.length);

Added TestRowDataConverter#testPreservesRowKind, which fails on the old code (expected: DELETE but was: INSERT) and passes with the fix.

Applied to flink/v2.3 only, following the convention used for the original RowData-evolution feature (#13340, backported to v1.19/v1.20 separately in #13401) — happy to backport to v1.20/v2.1/v2.2 in a follow-up if maintainers want it there too.

Closes #18100

RowDataConverter.convert() rebuilds rows whose schema differs from the
target table (reordered/missing/widened columns) via
`new GenericRowData(fieldGetters.length)`, which defaults to
RowKind.INSERT. Nothing restores the original kind afterwards, so a
converted DELETE/UPDATE_BEFORE row reaches BaseDeltaTaskWriter as an
INSERT: it is written as data instead of a delete, or NPEs if a
required non-key column is missing from the source row.

Carry sourceData.getRowKind() onto the converted row.

Closes apache#18100
@github-actions github-actions Bot added the flink label Sep 14, 2026
@pvary
pvary merged commit a061f8a into apache:main Sep 14, 2026
28 checks passed
@pvary

pvary commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Merged to main.
Thanks @lucasgameiroborges for the PR!
Please backport it to the other Flink versions as well.

pvary pushed a commit that referenced this pull request Sep 21, 2026
nickwu241 added a commit to Affirm/iceberg that referenced this pull request Sep 25, 2026
…-pick apache#18101) (#13)

* Flink: Preserve RowKind in DynamicIcebergSink's DataConverter

RowDataConverter.convert() rebuilt a row as new GenericRowData(fieldGetters.length)
whenever its schema didn't structurally match the target table's, which defaults
RowKind to INSERT and silently drops the source row's actual kind. A CDC
delete/UPDATE_BEFORE routed through this rebuild path was coerced into an INSERT.

Cherry-picked from apache#18101 (closes apache#18100).

* Flink: Backport RowKind-preservation fix to v1.20 and v2.0

v1.20/v2.1 are covered by apache#18196; v2.0 was never covered upstream
(the flink/v2.0 module was dropped from apache/iceberg main by apache#17849
before apache#18196 landed), so this is a manual port rather than a real cherry-pick.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flink: DynamicIcebergSink writes CDC delete records as inserts when the record needs schema conversion

2 participants