This is an automated email from the ASF dual-hosted git repository.
ahmedabu98 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 05c48794a86 allow dropping pk or watermark column (#40457)
05c48794a86 is described below
commit 05c48794a860fea77b640812b2d469da2310e7ed
Author: Ahmed Abualsaud <[email protected]>
AuthorDate: Mon Oct 19 11:36:54 2026 -0700
allow dropping pk or watermark column (#40457)
---
.../beam/sdk/io/iceberg/IcebergScanConfig.java | 78 ++++++++++++++-----
.../io/iceberg/cdc/IncrementalChangelogSource.java | 34 ++++++++-
.../IcebergCdcReadSchemaTransformProviderTest.java | 7 +-
.../beam/sdk/io/iceberg/IcebergIOReadTest.java | 18 -----
.../beam/sdk/io/iceberg/IcebergScanConfigTest.java | 77 +++++++++++++++----
.../cdc/IncrementalChangelogSourceTest.java | 87 +++++++++++++++++++++-
6 files changed, 239 insertions(+), 62 deletions(-)
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java
index bcd574afdab..6aebf0aa9b2 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfig.java
@@ -65,6 +65,7 @@ import org.joda.time.Duration;
public abstract class IcebergScanConfig implements Serializable {
private transient @MonotonicNonNull Table cachedTable;
private transient org.apache.iceberg.@MonotonicNonNull Schema
cachedProjectedSchema;
+ private transient org.apache.iceberg.@MonotonicNonNull Schema
cachedRequestedSchema;
private transient org.apache.iceberg.@MonotonicNonNull Schema
cachedRequiredSchema;
private transient @MonotonicNonNull Expression cachedFilter;
private transient org.apache.iceberg.@MonotonicNonNull Schema
cachedRecordIdSchema;
@@ -136,14 +137,25 @@ public abstract class IcebergScanConfig implements
Serializable {
return selectedFields.isEmpty() ? schema : schema.select(selectedFields);
}
- /** Returns the projected Schema after applying column pruning. */
+ /**
+ * Returns the projected Schema after applying column pruning. For CDC
reads, this also includes
+ * the primary key and watermark columns, even when they are not requested.
+ */
public org.apache.iceberg.Schema getProjectedSchema() {
if (cachedProjectedSchema == null) {
- cachedProjectedSchema = resolveSchema(getTable().schema(),
getKeepFields(), getDropFields());
+ cachedProjectedSchema = withCdcFields(getRequestedSchema());
}
return cachedProjectedSchema;
}
+ /** Returns the Schema of the columns selected by 'keep' or 'drop'. */
+ public org.apache.iceberg.Schema getRequestedSchema() {
+ if (cachedRequestedSchema == null) {
+ cachedRequestedSchema = resolveSchema(getTable().schema(),
getKeepFields(), getDropFields());
+ }
+ return cachedRequestedSchema;
+ }
+
/**
* Returns a Schema that includes all the fields required for a successful
read. This includes
* explicitly selected fields and fields referenced in the filter statement.
@@ -151,15 +163,54 @@ public abstract class IcebergScanConfig implements
Serializable {
public org.apache.iceberg.Schema getRequiredSchema() {
if (cachedRequiredSchema == null) {
cachedRequiredSchema =
- resolveSchema(
- getTable().schema(),
- getKeepFields(),
- getDropFields(),
- FilterUtils.getReferencedFieldNames(getFilterString()));
+ withCdcFields(
+ resolveSchema(
+ getTable().schema(),
+ getKeepFields(),
+ getDropFields(),
+ FilterUtils.getReferencedFieldNames(getFilterString())));
}
return cachedRequiredSchema;
}
+ /**
+ * CDC reads also need the primary key, whose column stats the changelog
scan uses to find update
+ * pairs, and the watermark column, which timestamps rows. Adds the
top-level columns holding them
+ * if the projection leaves them out.
+ */
+ private org.apache.iceberg.Schema withCdcFields(org.apache.iceberg.Schema
projection) {
+ if (!getUseCdc()) {
+ return projection;
+ }
+ org.apache.iceberg.Schema tableSchema = getTable().schema();
+ Set<Integer> missing = new HashSet<>(tableSchema.identifierFieldIds());
+ @Nullable String watermarkColumn = getWatermarkColumn();
+ @Nullable NestedField watermarkField =
+ watermarkColumn != null ? tableSchema.findField(watermarkColumn) :
null;
+ if (watermarkField != null) {
+ missing.add(watermarkField.fieldId());
+ }
+ missing.removeAll(TypeUtil.getProjectedIds(projection));
+ if (missing.isEmpty()) {
+ return projection;
+ }
+
+ Set<Integer> addedColumnIds = new HashSet<>();
+ for (NestedField column : TypeUtil.select(tableSchema, missing).columns())
{
+ addedColumnIds.add(column.fieldId());
+ }
+ List<NestedField> columns = new ArrayList<>();
+ for (NestedField column : tableSchema.columns()) {
+ @Nullable NestedField projected =
projection.asStruct().field(column.fieldId());
+ if (addedColumnIds.contains(column.fieldId())) {
+ columns.add(column);
+ } else if (projected != null) {
+ columns.add(projected);
+ }
+ }
+ return new org.apache.iceberg.Schema(columns);
+ }
+
public org.apache.iceberg.Schema recordIdSchema() {
if (cachedRecordIdSchema == null) {
org.apache.iceberg.Schema fullSchema = getTable().schema();
@@ -438,17 +489,9 @@ public abstract class IcebergScanConfig implements
Serializable {
+ invalidOptions));
}
} else {
- Set<Integer> primaryKeyIds = new
HashSet<>(table.schema().identifierFieldIds());
checkState(
- !primaryKeyIds.isEmpty(),
+ !table.schema().identifierFieldIds().isEmpty(),
"Cannot read CDC records as the table schema does not specified any
primary key fields.");
- Set<Integer> projectedFieldIds =
TypeUtil.getProjectedIds(getProjectedSchema());
- primaryKeyIds.removeAll(projectedFieldIds);
- checkArgument(
- primaryKeyIds.isEmpty(),
- "When reading CDC records, the projected schema must not drop
primary key fields. "
- + "The specified configuration drops the following PK fields:
%s",
- primaryKeyIds);
validateMetadataColumns(table);
}
@@ -527,9 +570,6 @@ public abstract class IcebergScanConfig implements
Serializable {
error("'watermark_column' must be a timestamp-typed column, but '%s'
has type %s"),
watermarkColumn,
field.type().typeId());
- checkArgumentNotNull(
- getProjectedSchema().findField(watermarkColumn),
- "'watermark_column' column should not be dropped.");
}
@Nullable String watermarkColumnTimeUnit = getWatermarkColumnTimeUnit();
diff --git
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java
index fe6240d5dc5..9a5d2413717 100644
---
a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java
+++
b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSource.java
@@ -31,7 +31,9 @@ import org.apache.beam.sdk.io.iceberg.IcebergScanConfig;
import org.apache.beam.sdk.io.iceberg.IcebergUtils;
import org.apache.beam.sdk.io.iceberg.ReadUtils;
import org.apache.beam.sdk.schemas.Schema;
+import org.apache.beam.sdk.schemas.transforms.Cast;
import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.PTransform;
import org.apache.beam.sdk.transforms.ParDo;
@@ -167,7 +169,21 @@ public class IncrementalChangelogSource extends
PTransform<PBegin, PCollection<R
watermarkColumn,
scanConfig.getWatermarkColumnTimeUnit())));
}
- return merged.setRowSchema(outputRowSchema);
+ Schema requestedRowSchema =
+ CdcOutputUtils.outputSchema(
+ scanConfig,
+ IcebergUtils.icebergSchemaToBeamSchema(
+ scanConfig.getRequestedSchema(),
scanConfig.getUpdateCompatibilityVersion()));
+ if (requestedRowSchema.equals(outputRowSchema)) {
+ return merged.setRowSchema(outputRowSchema);
+ }
+ // the primary key or watermark column was read only for internal use
+ return merged
+ .setRowSchema(outputRowSchema)
+ .apply(
+ "Drop Internal Columns",
+ ParDo.of(new DropInternalColumns(outputRowSchema,
requestedRowSchema)))
+ .setRowSchema(requestedRowSchema);
}
/**
@@ -208,4 +224,20 @@ public class IncrementalChangelogSource extends
PTransform<PBegin, PCollection<R
.collect(Collectors.toList());
return input.apply("Create Snapshot Range",
Create.timestamped(timestamped));
}
+
+ /** Drops columns read only for internal use. Rows keep their ValueKind and
timestamp. */
+ private static class DropInternalColumns extends DoFn<Row, Row> {
+ private final Schema inputSchema;
+ private final Schema outputSchema;
+
+ DropInternalColumns(Schema inputSchema, Schema outputSchema) {
+ this.inputSchema = inputSchema;
+ this.outputSchema = outputSchema;
+ }
+
+ @ProcessElement
+ public void process(@Element Row row, OutputReceiver<Row> out) {
+ out.output(Cast.castRow(row, inputSchema, outputSchema));
+ }
+ }
}
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java
index 3a21566c305..7a489b59a0b 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergCdcReadSchemaTransformProviderTest.java
@@ -230,7 +230,8 @@ public class IcebergCdcReadSchemaTransformProviderTest {
configMap.put("catalog_properties", properties);
configMap.put("from_snapshot", table.currentSnapshot().snapshotId());
configMap.put("to_snapshot", table.currentSnapshot().snapshotId());
- configMap.put("keep", ImmutableList.of("id", "data", "event_micros"));
+ // leaves out the primary key and the watermark column
+ configMap.put("keep", ImmutableList.of("data"));
configMap.put("filter", "\"category\" = 'include'");
configMap.put("watermark_column", "event_micros");
configMap.put(
@@ -241,7 +242,7 @@ public class IcebergCdcReadSchemaTransformProviderTest {
IcebergCdcMetadataColumns.ROW_ID,
IcebergCdcMetadataColumns.LAST_UPDATED_SEQUENCE_NUMBER));
- org.apache.iceberg.Schema projectedSchema = table.schema().select("id",
"data", "event_micros");
+ org.apache.iceberg.Schema projectedSchema = table.schema().select("data");
Schema recordSchema =
IcebergUtils.icebergSchemaToBeamSchema(projectedSchema);
Schema outputSchema =
Schema.builder()
@@ -263,9 +264,7 @@ public class IcebergCdcReadSchemaTransformProviderTest {
Row record =
IcebergUtils.icebergRecordToBeamRow(recordSchema, records.get(i));
return Row.withSchema(outputSchema)
.addValues(
- record.getInt64("id"),
record.getString("data"),
- record.getInt64("event_micros"),
snapshotId,
sequenceNumber,
firstRowId + i,
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java
index 7920fc37c84..9a8076bbb7d 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergIOReadTest.java
@@ -238,19 +238,6 @@ public class IcebergIOReadTest {
read.expand(PBegin.in(testPipeline));
}
- @Test
- public void testCdcFailsWhenProjectionDropsIdentifierFields() {
- assumeTrue(useIncrementalScan);
- TableIdentifier tableId = TableIdentifier.of("default",
testName.getMethodName());
- warehouse.createTable(tableId, schemaForMode(TestFixtures.SCHEMA, 1));
- IcebergIO.ReadRows read =
-
IcebergIO.readRows(catalogConfig()).from(tableId).withCdc().dropping(singletonList("id"));
-
- thrown.expect(IllegalArgumentException.class);
- thrown.expectMessage("projected schema must not drop primary key fields");
- read.expand(PBegin.in(testPipeline));
- }
-
@Test
public void testFailWhenDropAndKeepAreSet() {
TableIdentifier tableId = TableIdentifier.of("default",
testName.getMethodName());
@@ -410,11 +397,6 @@ public class IcebergIOReadTest {
return null;
});
- if (useIncrementalScan) {
- testPipeline.run();
- return;
- }
-
// test drop fields
read = read.keeping(null).dropping(singletonList("id"));
PCollection<Row> outputDrop =
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java
index e928d3cf8f0..989e08dd35d 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/IcebergScanConfigTest.java
@@ -20,6 +20,7 @@ package org.apache.beam.sdk.io.iceberg;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import java.util.List;
@@ -76,31 +77,77 @@ public class IcebergScanConfigTest {
}
@Test
- public void cdcValidationRejectsProjectionDroppingIdentifierFields() {
+ public void cdcProjectionReadsKeyAndWatermarkLeftOutOfKeepOrDrop() {
TableIdentifier tableId = uniqueTableId();
Table table = warehouse.createTable(tableId, CDC_SCHEMA);
- IcebergScanConfig keepWithoutPk =
+ IcebergScanConfig keep =
scanConfigBuilder(tableId, CDC_SCHEMA)
.setUseCdc(true)
.setKeepFields(ImmutableList.of("data"))
+ .setWatermarkColumn("event_time")
+ .setFilterString("\"category\" = 'x'")
.build();
- IllegalArgumentException keepException =
- assertThrows(IllegalArgumentException.class, () ->
keepWithoutPk.validate(table));
- assertThat(
- keepException.getMessage(),
- containsString("projected schema must not drop primary key fields"));
+ keep.validate(table);
+ assertEquals(ImmutableList.of("data"),
fieldNames(keep.getRequestedSchema()));
+ assertEquals(
+ ImmutableList.of("id", "data", "event_time"),
fieldNames(keep.getProjectedSchema()));
+ assertEquals(
+ ImmutableList.of("id", "data", "category", "event_time"),
+ fieldNames(keep.getRequiredSchema()));
+
+ IcebergScanConfig drop =
+ scanConfigBuilder(tableId, CDC_SCHEMA)
+ .setUseCdc(true)
+ .setDropFields(ImmutableList.of("id", "event_time"))
+ .setWatermarkColumn("event_time")
+ .build();
+ drop.validate(table);
+ assertEquals(
+ ImmutableList.of(
+ "data", "category", "event_micros", "optional_time",
"required_text", "nested"),
+ fieldNames(drop.getRequestedSchema()));
+ assertEquals(fieldNames(CDC_SCHEMA),
fieldNames(drop.getProjectedSchema()));
- IcebergScanConfig dropPk =
+ // a projection that already has both is unchanged
+ IcebergScanConfig unchanged =
scanConfigBuilder(tableId, CDC_SCHEMA)
.setUseCdc(true)
- .setDropFields(ImmutableList.of("id"))
+ .setKeepFields(ImmutableList.of("id", "event_time"))
+ .setWatermarkColumn("event_time")
.build();
- IllegalArgumentException dropException =
- assertThrows(IllegalArgumentException.class, () ->
dropPk.validate(table));
- assertThat(
- dropException.getMessage(),
- containsString("projected schema must not drop primary key fields"));
+ assertSame(unchanged.getRequestedSchema(), unchanged.getProjectedSchema());
+
+ // batch reads only project the requested columns
+ IcebergScanConfig batch =
+ scanConfigBuilder(tableId,
CDC_SCHEMA).setKeepFields(ImmutableList.of("data")).build();
+ assertEquals(ImmutableList.of("data"),
fieldNames(batch.getProjectedSchema()));
+ }
+
+ @Test
+ public void cdcProjectionReadsWholeColumnHoldingNestedKey() {
+ TableIdentifier tableId = uniqueTableId();
+ Table table =
+ warehouse.createTable(
+ tableId,
+ new org.apache.iceberg.Schema(
+ ImmutableList.of(
+ Types.NestedField.required(
+ 1,
+ "key",
+ Types.StructType.of(
+ Types.NestedField.required(2, "id",
Types.LongType.get()),
+ Types.NestedField.optional(3, "region",
Types.StringType.get()))),
+ Types.NestedField.optional(4, "data",
Types.StringType.get())),
+ ImmutableSet.of(2)));
+ IcebergScanConfig scanConfig =
+ scanConfigBuilder(tableId, table.schema())
+ .setUseCdc(true)
+ .setKeepFields(ImmutableList.of("data"))
+ .build();
+
+ // whole columns, as ResolveChanges emits them
+ assertEquals(table.schema().asStruct(),
scanConfig.getProjectedSchema().asStruct());
}
@Test
@@ -219,8 +266,6 @@ public class IcebergScanConfigTest {
true,
ImmutableList.of("id", "required_text"),
"must be a timestamp-typed column");
- assertInvalidWatermark(
- tableId, table, "event_time", true, ImmutableList.of("id"), "should
not be dropped");
}
private void assertInvalidWatermark(
diff --git
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java
index 27a65a0be8f..c055e609f76 100644
---
a/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java
+++
b/sdks/java/io/iceberg/src/test/java/org/apache/beam/sdk/io/iceberg/cdc/IncrementalChangelogSourceTest.java
@@ -460,6 +460,79 @@ public class IncrementalChangelogSourceTest {
pipeline.run().waitUntilFinish();
}
+ /**
+ * 'keep' and 'drop' may leave out the primary key and watermark column. The
source still reads
+ * them to pair updates and timestamp rows.
+ */
+ @Test
+ public void keyAndWatermarkCanBeLeftOutOfOutput() throws Exception {
+ LocalDateTime t1 =
+ LocalDateTime.ofInstant(
+ java.time.Instant.ofEpochMilli(System.currentTimeMillis() -
3_000L), ZoneOffset.UTC);
+ LocalDateTime t2 = t1.plusSeconds(1);
+ LocalDateTime t3 = t1.plusSeconds(2);
+ // SPLIT_SIZE=1 resolves updates through the CoGroupByKey, the default
resolves them locally
+ Map<String, IcebergScanConfig> reads =
+ ImmutableMap.of(
+ "Keep",
+ eventChanges("1", t1, t2,
t3).setKeepFields(ImmutableList.of("data")).build(),
+ "Drop",
+ eventChanges(String.valueOf(TableProperties.SPLIT_SIZE_DEFAULT),
t1, t2, t3)
+ .setDropFields(ImmutableList.of("id", "event_time"))
+ .build());
+
+ for (Map.Entry<String, IcebergScanConfig> read : reads.entrySet()) {
+ PCollection<Row> rows =
+ pipeline.apply(read.getKey(), new
IncrementalChangelogSource(read.getValue()));
+ assertEquals(
+ Schema.builder().addNullableField("data",
Schema.FieldType.STRING).build(),
+ rows.getSchema());
+ PAssert.that(
+ rows.apply(read.getKey() + " Format", ParDo.of(new
FormatValueKindAndValues()))
+ .apply(read.getKey() + " Timestamps", Reify.timestamps()))
+ .containsInAnyOrder(
+ TimestampedValue.of("INSERT:[a]", millis(t1)),
+ TimestampedValue.of("INSERT:[b]", millis(t2)),
+ // only event_time changed, so pairing compared full rows
+ TimestampedValue.of("UPDATE_BEFORE:[a]", millis(t1)),
+ TimestampedValue.of("UPDATE_AFTER:[a]", millis(t3)),
+ TimestampedValue.of("DELETE:[b]", millis(t2)),
+ TimestampedValue.of("INSERT:[c]", millis(t3)));
+ }
+
+ pipeline.run().waitUntilFinish();
+ }
+
+ /**
+ * Commits id 1 and 2, then an overwrite that changes only id 1's
event_time, deletes 2, adds 3.
+ */
+ private IcebergScanConfig.Builder eventChanges(
+ String splitSize, LocalDateTime t1, LocalDateTime t2, LocalDateTime t3)
throws IOException {
+ TableIdentifier tableId = TableIdentifier.of("default",
testName.getMethodName() + splitSize);
+ Table table =
+ warehouse.createTable(
+ tableId,
+ EVENT_SCHEMA,
+ null,
+ ImmutableMap.of(
+ TableProperties.FORMAT_VERSION, "2",
TableProperties.SPLIT_SIZE, splitSize));
+ DataFile v1 =
+ commitAppend(
+ table,
+ splitSize + "-v1.parquet",
+ ImmutableList.of(eventRecord(1L, "a", t1), eventRecord(2L, "b",
t2)));
+ commitOverwrite(
+ table, splitSize + "-v2.parquet", v1, eventRecord(1L, "a", t3),
eventRecord(3L, "c", t3));
+ return baseConfigBuilder(table, tableId)
+ .setWatermarkColumn("event_time")
+ .setStartingStrategy(StartingStrategy.EARLIEST)
+ .setToSnapshot(table.currentSnapshot().snapshotId());
+ }
+
+ private static Instant millis(LocalDateTime time) {
+ return new Instant(time.toInstant(ZoneOffset.UTC).toEpochMilli());
+ }
+
/**
* Bi-directional rows are windowed per snapshot with zero allowed lateness,
so a record that
* reaches the CoGroupByKey after the watermark has passed its snapshot's
window is dropped
@@ -516,12 +589,10 @@ public class IncrementalChangelogSourceTest {
}
private DataFile commitOverwrite(
- Table table, String fileName, DataFile replaced, Record replacement)
throws IOException {
+ Table table, String fileName, DataFile replaced, Record... replacements)
throws IOException {
DataFile file =
warehouse.writeRecords(
- testName.getMethodName() + "-" + fileName,
- table.schema(),
- ImmutableList.of(replacement));
+ testName.getMethodName() + "-" + fileName, table.schema(),
Arrays.asList(replacements));
table.newOverwrite().deleteFile(replaced).addFile(file).commit();
table.refresh();
return file;
@@ -583,6 +654,14 @@ public class IncrementalChangelogSourceTest {
}
}
+ private static final class FormatValueKindAndValues extends DoFn<Row,
String> {
+ @ProcessElement
+ public void process(
+ @Element Row row, ValueKind valueKind, OutputReceiver<String>
outputReceiver) {
+ outputReceiver.output(valueKind.name() + ":" + row.getValues());
+ }
+ }
+
private static final class FormatDefaultedChange extends DoFn<Row, String> {
@ProcessElement
public void process(