-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Flink: Add streaming upsert write option. #2863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
b047073
bab1b22
9d3d1ed
49835e1
34f02c7
dcdd403
28c18f3
8e397e3
4391ba7
4d33f18
56822c8
9ae91cc
8a8a812
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> { | |
| private final Schema schema; | ||
| private final Schema deleteSchema; | ||
| private final RowDataWrapper wrapper; | ||
| private final boolean upsert; | ||
|
|
||
| BaseDeltaTaskWriter(PartitionSpec spec, | ||
| FileFormat format, | ||
|
|
@@ -50,11 +51,13 @@ abstract class BaseDeltaTaskWriter extends BaseTaskWriter<RowData> { | |
| long targetFileSize, | ||
| Schema schema, | ||
| RowType flinkSchema, | ||
| List<Integer> equalityFieldIds) { | ||
| List<Integer> equalityFieldIds, | ||
| boolean upsert) { | ||
| super(spec, format, appenderFactory, fileFactory, io, targetFileSize); | ||
| this.schema = schema; | ||
| this.deleteSchema = TypeUtil.select(schema, Sets.newHashSet(equalityFieldIds)); | ||
| this.wrapper = new RowDataWrapper(flinkSchema, schema.asStruct()); | ||
| this.upsert = upsert; | ||
| } | ||
|
|
||
| abstract RowDataDeltaWriter route(RowData row); | ||
|
|
@@ -69,6 +72,9 @@ public void write(RowData row) throws IOException { | |
|
|
||
| switch (row.getRowKind()) { | ||
| case INSERT: | ||
| if (upsert) { | ||
| writer.delete(row); | ||
| } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could only delete row on INSERT. I don't think there will be only have
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For an update operation in flink, the For the downstream iceberg sink, we need to handle all the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can only delete row on
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By name, I thought INSERT is "add a new row". Then we don't need to add a delete for it. But I guess it actually means "append a row (new or updated)".
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @openinx mentioned in #1996 (comment), we need to transform INSERT/UPDATE_AFTER to be UPSERT(delete + insert). If we don't add a delete on INSERT row when upsert mode is enable, we will get duplicate rows for same primary key. |
||
| case UPDATE_AFTER: | ||
| writer.write(row); | ||
| break; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -41,6 +41,7 @@ | |||
| import org.apache.iceberg.DistributionMode; | ||||
| import org.apache.iceberg.FileFormat; | ||||
| import org.apache.iceberg.PartitionSpec; | ||||
| import org.apache.iceberg.PartitionField; | ||||
|
Reo-LEI marked this conversation as resolved.
|
||||
| import org.apache.iceberg.Schema; | ||||
| import org.apache.iceberg.Table; | ||||
| import org.apache.iceberg.flink.FlinkSchemaUtil; | ||||
|
|
@@ -124,6 +125,7 @@ public static class Builder { | |||
| private boolean overwrite = false; | ||||
| private DistributionMode distributionMode = null; | ||||
| private Integer writeParallelism = null; | ||||
| private boolean upsert = false; | ||||
| private List<String> equalityFieldColumns = null; | ||||
| private String uidPrefix = null; | ||||
|
|
||||
|
|
@@ -196,6 +198,20 @@ public Builder writeParallelism(int newWriteParallelism) { | |||
| return this; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * All INSERT/UPDATE_AFTER events from input stream will be transformed to UPSERT events, which means it will | ||||
| * DELETE the old records and then INSERT the new records. In partitioned table, the partition fields should be | ||||
| * a subset of equality fields, otherwise the old row that located in partition-A could not be deleted by the | ||||
| * new row that located in partition-B. | ||||
| * | ||||
| * @param enable indicate whether it should transform all INSERT/UPDATE_AFTER events to UPSERT. | ||||
| * @return {@link Builder} to connect the iceberg table. | ||||
| */ | ||||
| public Builder upsert(boolean enable) { | ||||
| this.upsert = enable; | ||||
| return this; | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Configuring the equality field columns for iceberg table that accept CDC or UPSERT events. | ||||
| * | ||||
|
|
@@ -263,8 +279,20 @@ public DataStreamSink<RowData> build() { | |||
| // Distribute the records from input data stream based on the write.distribution-mode. | ||||
| rowDataInput = distributeDataStream(rowDataInput, table.properties(), table.spec(), table.schema(), flinkRowType); | ||||
|
|
||||
| // Convert the INSERT stream to be an UPSERT stream if needed. | ||||
|
Reo-LEI marked this conversation as resolved.
Outdated
|
||||
| if (upsert) { | ||||
| Preconditions.checkState(!equalityFieldIds.isEmpty(), | ||||
| "Equality field columns shouldn't be empty when configuring to use UPSERT data stream."); | ||||
| if (!table.spec().isUnpartitioned()) { | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for my own learning, does partition field must be included in equality fields? e.g., we can have an equality field (like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As @openinx comment as above, we shoule restrict the partition fields is a subset of equality fields to ensure we can delete the old data in same partition.
I think that is not a valid scenario, to keep
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we have a table with If we just partition the table by |
||||
| for (PartitionField partitionField : table.spec().fields()) { | ||||
| Preconditions.checkState(equalityFieldIds.contains(partitionField.sourceId()), | ||||
| "Partition field '%s' is not included in equality fields: '%s'", partitionField, equalityFieldColumns); | ||||
| } | ||||
| } | ||||
| } | ||||
|
|
||||
| // Chain the iceberg stream writer and committer operator. | ||||
| IcebergStreamWriter<RowData> streamWriter = createStreamWriter(table, flinkRowType, equalityFieldIds); | ||||
| IcebergStreamWriter<RowData> streamWriter = createStreamWriter(table, flinkRowType, equalityFieldIds, upsert); | ||||
| IcebergFilesCommitter filesCommitter = new IcebergFilesCommitter(tableLoader, overwrite); | ||||
|
|
||||
| this.writeParallelism = writeParallelism == null ? rowDataInput.getParallelism() : writeParallelism; | ||||
|
|
@@ -351,14 +379,16 @@ static RowType toFlinkRowType(Schema schema, TableSchema requestedSchema) { | |||
|
|
||||
| static IcebergStreamWriter<RowData> createStreamWriter(Table table, | ||||
| RowType flinkRowType, | ||||
| List<Integer> equalityFieldIds) { | ||||
| List<Integer> equalityFieldIds, | ||||
| boolean upsert) { | ||||
| Preconditions.checkArgument(table != null, "Iceberg table should't be null"); | ||||
| Map<String, String> props = table.properties(); | ||||
| long targetFileSize = getTargetFileSizeBytes(props); | ||||
| FileFormat fileFormat = getFileFormat(props); | ||||
|
|
||||
| TaskWriterFactory<RowData> taskWriterFactory = new RowDataTaskWriterFactory(table.schema(), flinkRowType, | ||||
| table.spec(), table.locationProvider(), table.io(), table.encryption(), targetFileSize, fileFormat, props, | ||||
| equalityFieldIds); | ||||
| equalityFieldIds, upsert); | ||||
|
|
||||
| return new IcebergStreamWriter<>(table.name(), taskWriterFactory); | ||||
| } | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.