diff --git a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CollectDistinctSchemas.java b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CollectDistinctSchemas.java
index 1b81e008f19d..5c36967cfeba 100644
--- a/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CollectDistinctSchemas.java
+++ b/sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/CollectDistinctSchemas.java
@@ -17,87 +17,219 @@
*/
package org.apache.beam.sdk.io.iceberg;
+import com.google.auto.value.AutoValue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.TreeMap;
+import java.util.TreeSet;
+import org.apache.beam.sdk.coders.AtomicCoder;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.coders.CoderRegistry;
-import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.ListCoder;
import org.apache.beam.sdk.coders.MapCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarLongCoder;
+import org.apache.beam.sdk.schemas.AutoValueSchema;
+import org.apache.beam.sdk.schemas.NoSuchSchemaException;
+import org.apache.beam.sdk.schemas.SchemaCoder;
+import org.apache.beam.sdk.schemas.SchemaRegistry;
+import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
+import org.apache.beam.sdk.schemas.annotations.SchemaFieldNumber;
import org.apache.beam.sdk.transforms.Combine;
-import org.apache.beam.sdk.values.KV;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
/**
- * Collects the distinct schemas among canonical file schema JSONs (see {@link FileSchemas}), with
- * the number of files per schema, most common first (ties broken by JSON). The commit side applies
- * schemas in this order, so the schema covering the most files wins a conflict.
- *
- *
Inputs are compared as strings, so they must already be canonical.
+ * One output entry per distinct schema: its file count and the columns EVERY file carrying it
+ * proved free of nulls; one file with a null in "name" forces "name" to relax, however many clean
+ * files sit next to it. Entries come out most common first (ties broken by the JSON text) because
+ * the commit side applies schemas in that order and the most common schema should win a conflict.
+ * Schemas are compared as strings, so inputs must already be canonical.
*/
class CollectDistinctSchemas
- extends Combine.CombineFn, List>> {
+ extends Combine.CombineFn<
+ CollectDistinctSchemas.SchemaGroup,
+ Map,
+ List> {
+
+ /** Mutable accumulator counterpart of {@link SchemaGroup}. */
+ static final class Group {
+ long files;
+ TreeSet nullFreeColumns;
+
+ Group(long files, TreeSet nullFreeColumns) {
+ this.files = files;
+ this.nullFreeColumns = nullFreeColumns;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (!(other instanceof Group)) {
+ return false;
+ }
+ Group that = (Group) other;
+ return files == that.files && nullFreeColumns.equals(that.nullFreeColumns);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(files, nullFreeColumns);
+ }
+ }
+
+ /**
+ * A schema, how many files carry it, and the columns all of them proved free of nulls.
+ * ReadFooterSchema emits one per file ({@code files} = 1); this combiner merges them.
+ */
+ @DefaultSchema(AutoValueSchema.class)
+ @AutoValue
+ abstract static class SchemaGroup {
+ private static @MonotonicNonNull SchemaCoder coder;
+
+ static SchemaGroup of(String schemaJson, long files, List nullFreeColumns) {
+ return new AutoValue_CollectDistinctSchemas_SchemaGroup(schemaJson, files, nullFreeColumns);
+ }
+
+ static SchemaCoder getCoder() {
+ if (coder == null) {
+ try {
+ coder = SchemaRegistry.createDefault().getSchemaCoder(SchemaGroup.class);
+ } catch (NoSuchSchemaException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return coder;
+ }
+
+ @SchemaFieldNumber("0")
+ abstract String getSchemaJson();
+
+ @SchemaFieldNumber("1")
+ abstract long getFiles();
+
+ @SchemaFieldNumber("2")
+ abstract List getNullFreeColumns();
+
+ @Override
+ public final String toString() {
+ return getFiles()
+ + " file(s), null-free in "
+ + getNullFreeColumns()
+ + ", schema "
+ + getSchemaJson();
+ }
+ }
@Override
- public Map createAccumulator() {
+ public Map createAccumulator() {
return new TreeMap<>();
}
@Override
- public Map addInput(Map accumulator, String schemaJson) {
- add(accumulator, schemaJson, 1L);
+ public Map addInput(Map accumulator, SchemaGroup file) {
+ add(accumulator, file.getSchemaJson(), file.getFiles(), file.getNullFreeColumns());
return accumulator;
}
@Override
- public Map mergeAccumulators(Iterable