, DataType> selectedSchemas() {
+ return selectedSchemas;
+ }
+ }
+
/**
* Given the schema of a Variant type, finalize the schema. Specifically: 1) Widen integer types
* to LongType 2) Replace empty rows with VariantType 3) Limit the total number of shredded
diff --git a/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingInferenceSession.java b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingInferenceSession.java
new file mode 100644
index 000000000000..ba4292d37829
--- /dev/null
+++ b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingInferenceSession.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.data.variant;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.Preconditions;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Rolling-writer-scoped Variant inference state.
+ *
+ * The state is intentionally not persisted or shared between rolling writers.
+ */
+public class VariantShreddingInferenceSession {
+
+ private final InferVariantShreddingSchema inferrer;
+ private final int effectiveSampleSize;
+ private final double admissionRatio;
+ private final double retentionRatio;
+
+ private InferVariantShreddingSchema.InferenceEvidence committedEvidence;
+ private Map, DataType> committedSelectedSchemas = Collections.emptyMap();
+ private InferVariantShreddingSchema.AdaptiveInferenceResult pendingResult;
+
+ public VariantShreddingInferenceSession(
+ InferVariantShreddingSchema inferrer,
+ int effectiveSampleSize,
+ double admissionRatio,
+ double retentionRatio) {
+ Preconditions.checkArgument(
+ effectiveSampleSize > 0, "Effective sample size must be positive.");
+ Preconditions.checkArgument(
+ admissionRatio >= 0 && admissionRatio <= 1,
+ "Admission ratio must be between 0 and 1.");
+ Preconditions.checkArgument(
+ retentionRatio >= 0 && retentionRatio <= admissionRatio,
+ "Retention ratio must be between 0 and the admission ratio.");
+ this.inferrer = inferrer;
+ this.effectiveSampleSize = effectiveSampleSize;
+ this.admissionRatio = admissionRatio;
+ this.retentionRatio = retentionRatio;
+ }
+
+ public boolean hasPrior() {
+ return committedEvidence != null;
+ }
+
+ public RowType inferSchema(List rows) {
+ if (committedEvidence == null) {
+ pendingResult = inferrer.inferInitial(rows, effectiveSampleSize);
+ } else {
+ pendingResult =
+ inferrer.inferAdaptive(
+ committedEvidence,
+ committedSelectedSchemas,
+ rows,
+ effectiveSampleSize,
+ admissionRatio,
+ retentionRatio);
+ }
+ return pendingResult.physicalRowType();
+ }
+
+ public void commitPendingInference() {
+ Preconditions.checkState(pendingResult != null, "No pending Variant inference to commit.");
+ committedEvidence = pendingResult.evidence();
+ committedSelectedSchemas = pendingResult.selectedSchemas();
+ pendingResult = null;
+ }
+}
diff --git a/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java b/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
index 5f497d2d9222..22f102d814b5 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
@@ -126,7 +126,7 @@ private void finalizePlanAndFlush() throws IOException {
ShreddingWritePlan writePlan = writePlanFactory.createWritePlan(collectAllRows());
actualWriter =
ShreddingWritePlanWriterFactory.createWriterWithPlan(
- writerFactory, out, compression, writePlan);
+ writerFactory, writePlanFactory, out, compression, writePlan);
planFinalized = true;
if (!bufferedBundles.isEmpty()) {
diff --git a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingFormatWriter.java b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingFormatWriter.java
index 504852819fa4..97a9d4d2da9d 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingFormatWriter.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingFormatWriter.java
@@ -35,16 +35,19 @@ public class ShreddingFormatWriter implements BundleFormatWriter {
private final FormatWriter delegate;
private final SupportsShreddingWritePlan writerFactory;
+ private final ShreddingWritePlanFactory writePlanFactory;
private final ShreddingWritePlan writePlan;
private final String compression;
public ShreddingFormatWriter(
FormatWriter delegate,
SupportsShreddingWritePlan writerFactory,
+ ShreddingWritePlanFactory writePlanFactory,
ShreddingWritePlan writePlan,
String compression) {
this.delegate = delegate;
this.writerFactory = writerFactory;
+ this.writePlanFactory = writePlanFactory;
this.writePlan = writePlan;
this.compression = compression;
}
@@ -84,6 +87,7 @@ public void close() throws IOException {
} finally {
delegate.close();
}
+ writePlanFactory.onFileCompleted(writePlan);
}
private class PhysicalBundleRecords implements BundleRecords {
diff --git a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanFactory.java b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanFactory.java
index 5adb313fe6a4..f2490fd04654 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanFactory.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanFactory.java
@@ -36,4 +36,12 @@ public interface ShreddingWritePlanFactory {
int inferBufferRowCount();
ShreddingWritePlan createWritePlan(List sampleRows);
+
+ /**
+ * Reports that a file using the given write plan has been closed successfully.
+ *
+ * Implementations may use this callback to advance rolling-writer-scoped state. A factory is
+ * used sequentially by one rolling writer and is not thread-safe.
+ */
+ default void onFileCompleted(ShreddingWritePlan writePlan) {}
}
diff --git a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactory.java b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactory.java
index 5441eb4a9799..9b4c6211f3d2 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactory.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactory.java
@@ -55,6 +55,7 @@ public FormatWriter create(PositionOutputStream out, String compression) throws
return createWriterWithPlan(
delegate,
+ writePlanFactory,
out,
compression,
writePlanFactory.createWritePlan(Collections.emptyList()));
@@ -62,6 +63,7 @@ public FormatWriter create(PositionOutputStream out, String compression) throws
static FormatWriter createWriterWithPlan(
SupportsShreddingWritePlan delegate,
+ ShreddingWritePlanFactory writePlanFactory,
PositionOutputStream out,
String compression,
ShreddingWritePlan writePlan)
@@ -69,6 +71,7 @@ static FormatWriter createWriterWithPlan(
return new ShreddingFormatWriter(
delegate.createWithShreddingWritePlan(out, compression, writePlan),
delegate,
+ writePlanFactory,
writePlan,
compression);
}
diff --git a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java b/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
index 75e4c77e6832..357248c90a20 100644
--- a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
+++ b/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
@@ -22,6 +22,7 @@
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.shredding.ShreddingWritePlan;
import org.apache.paimon.data.variant.InferVariantShreddingSchema;
+import org.apache.paimon.data.variant.VariantShreddingInferenceSession;
import org.apache.paimon.data.variant.VariantShreddingWritePlan;
import org.apache.paimon.format.shredding.ShreddingWritePlanFactory;
import org.apache.paimon.options.Options;
@@ -30,6 +31,9 @@
import org.apache.paimon.types.RowType;
import org.apache.paimon.types.VariantType;
import org.apache.paimon.utils.JsonSerdeUtil;
+import org.apache.paimon.utils.Preconditions;
+
+import javax.annotation.Nullable;
import java.util.List;
@@ -38,10 +42,25 @@ public class VariantShreddingWritePlanFactory implements ShreddingWritePlanFacto
private final RowType rowType;
private final Options options;
+ @Nullable private final VariantShreddingInferenceSession adaptiveSession;
+ @Nullable private ShreddingWritePlan pendingAdaptivePlan;
public VariantShreddingWritePlanFactory(RowType rowType, Options options) {
this.rowType = rowType;
this.options = options;
+ if (isAdaptiveInference() && shouldInferWritePlan()) {
+ double admissionRatio =
+ options.get(CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO);
+ this.adaptiveSession =
+ new VariantShreddingInferenceSession(
+ createInferrer(),
+ options.get(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW),
+ admissionRatio,
+ options.get(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO));
+ } else {
+ this.adaptiveSession = null;
+ }
}
@Override
@@ -69,6 +88,9 @@ public boolean shouldInferWritePlan() {
@Override
public int inferBufferRowCount() {
+ if (adaptiveSession != null && adaptiveSession.hasPrior()) {
+ return options.get(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW);
+ }
return options.get(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW);
}
@@ -79,14 +101,41 @@ public ShreddingWritePlan createWritePlan(List sampleRows) {
rowType, configuredShreddingSchema());
}
- RowType physicalRowType = createInferrer().inferSchema(sampleRows);
- return new VariantShreddingWritePlan(rowType, physicalRowType);
+ RowType physicalRowType;
+ if (adaptiveSession == null) {
+ physicalRowType = createInferrer().inferSchema(sampleRows);
+ } else {
+ physicalRowType = adaptiveSession.inferSchema(sampleRows);
+ }
+ VariantShreddingWritePlan writePlan =
+ new VariantShreddingWritePlan(rowType, physicalRowType);
+ if (adaptiveSession != null) {
+ pendingAdaptivePlan = writePlan;
+ }
+ return writePlan;
+ }
+
+ @Override
+ public void onFileCompleted(ShreddingWritePlan writePlan) {
+ if (adaptiveSession == null) {
+ return;
+ }
+ Preconditions.checkState(
+ writePlan == pendingAdaptivePlan,
+ "Completed Variant write plan does not match the pending inference.");
+ adaptiveSession.commitPendingInference();
+ pendingAdaptivePlan = null;
}
private boolean hasConfiguredShreddingSchema() {
return options.contains(CoreOptions.VARIANT_SHREDDING_SCHEMA);
}
+ private boolean isAdaptiveInference() {
+ return options.get(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE)
+ == CoreOptions.VariantShreddingInferenceMode.ADAPTIVE;
+ }
+
private RowType configuredShreddingSchema() {
String shreddingSchema = options.get(CoreOptions.VARIANT_SHREDDING_SCHEMA);
return (RowType) JsonSerdeUtil.fromJson(shreddingSchema, DataType.class);
diff --git a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
index 1652e8171719..e10afa965217 100644
--- a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
@@ -82,7 +82,7 @@ void testConvertAndBuildFieldMetadata() {
}
@Test
- void testFactoryInfersColumnCountFromFirstRow() {
+ void testFactoryUsesMaxColumnCountForFirstFile() {
RowType logicalType =
DataTypes.ROW(
DataTypes.FIELD(
@@ -90,8 +90,8 @@ void testFactoryInfersColumnCountFromFirstRow() {
MapSharedShreddingWritePlanFactory factory = createFactory(logicalType, 4);
assertThat(factory.shouldCreateWritePlan()).isTrue();
- assertThat(factory.shouldInferWritePlan()).isTrue();
- assertThat(factory.inferBufferRowCount()).isEqualTo(1);
+ assertThat(factory.shouldInferWritePlan()).isFalse();
+ assertThat(factory.inferBufferRowCount()).isZero();
assertThat(
factory.createWritePlan(
Collections.singletonList(
@@ -100,26 +100,31 @@ void testFactoryInfersColumnCountFromFirstRow() {
.physicalRowType())
.isEqualTo(
MapSharedShreddingUtils.logicalToPhysicalSchema(
- logicalType, Collections.singletonMap("tags", 3)));
+ logicalType, Collections.singletonMap("tags", 4)));
}
@Test
- void testFactoryCapsInferredColumnCountAtMaxColumns() {
+ void testFactoryUsesCompletedFileStatisticsForNextFile() {
RowType logicalType =
DataTypes.ROW(
DataTypes.FIELD(
0, "tags", DataTypes.MAP(DataTypes.STRING(), DataTypes.INT())));
- MapSharedShreddingWritePlanFactory factory = createFactory(logicalType, 2);
+ MapSharedShreddingWritePlanFactory factory = createFactory(logicalType, 8);
- assertThat(
- factory.createWritePlan(
- Collections.singletonList(
- GenericRow.of(
- stringKeyMap("a", 1, "b", 2, "c", 3))))
- .physicalRowType())
+ ShreddingWritePlan firstPlan = factory.createWritePlan(Collections.emptyList());
+ firstPlan.toPhysicalRow(GenericRow.of(stringKeyMap("a", 1, "b", 2))).getRow(0, 10);
+ firstPlan.toPhysicalRow(GenericRow.of(stringKeyMap("c", 3, "d", 4, "e", 5))).getRow(0, 10);
+ factory.onFileCompleted(firstPlan);
+
+ ShreddingWritePlan secondPlan = factory.createWritePlan(Collections.emptyList());
+ assertThat(firstPlan.physicalRowType())
.isEqualTo(
MapSharedShreddingUtils.logicalToPhysicalSchema(
- logicalType, Collections.singletonMap("tags", 2)));
+ logicalType, Collections.singletonMap("tags", 8)));
+ assertThat(secondPlan.physicalRowType())
+ .isEqualTo(
+ MapSharedShreddingUtils.logicalToPhysicalSchema(
+ logicalType, Collections.singletonMap("tags", 3)));
}
@Test
diff --git a/paimon-common/src/test/java/org/apache/paimon/data/variant/InferVariantShreddingSchemaTest.java b/paimon-common/src/test/java/org/apache/paimon/data/variant/InferVariantShreddingSchemaTest.java
index 68787f8a4512..ad541b0157ec 100644
--- a/paimon-common/src/test/java/org/apache/paimon/data/variant/InferVariantShreddingSchemaTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/data/variant/InferVariantShreddingSchemaTest.java
@@ -195,6 +195,103 @@ void testInferSchemaWithEmptyRows() {
.isEqualTo(variantShreddingSchema(DataTypes.VARIANT()));
}
+ @Test
+ void testAdaptiveInferenceUsesAdmissionAndRetentionThresholds() {
+ RowType schema = RowType.of(new DataType[] {DataTypes.VARIANT()}, new String[] {"v"});
+ VariantShreddingInferenceSession session =
+ new VariantShreddingInferenceSession(
+ new InferVariantShreddingSchema(schema, 300, 50, 0.4), 10, 0.4, 0.2);
+
+ List initialRows = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ initialRows.add(
+ GenericRow.of(
+ GenericVariant.fromJson(
+ i < 5 ? "{\"legacy\":\"v\",\"stable\":1}" : "{\"stable\":1}")));
+ }
+ RowType initialSchema = session.inferSchema(initialRows);
+ assertThat(initialSchema.getField("v").type())
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.STRING(), DataTypes.BIGINT()},
+ new String[] {"legacy", "stable"})));
+ session.commitPendingInference();
+
+ List secondRows = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ secondRows.add(
+ GenericRow.of(
+ GenericVariant.fromJson(
+ i < 9
+ ? "{\"emerging\":true,\"stable\":2}"
+ : "{\"stable\":2}")));
+ }
+ RowType secondSchema = session.inferSchema(secondRows);
+ assertThat(secondSchema.getField("v").type())
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {
+ DataTypes.BOOLEAN(),
+ DataTypes.STRING(),
+ DataTypes.BIGINT()
+ },
+ new String[] {"emerging", "legacy", "stable"})));
+ session.commitPendingInference();
+
+ List thirdRows = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ thirdRows.add(GenericRow.of(GenericVariant.fromJson("{\"stable\":3}")));
+ }
+ RowType thirdSchema = session.inferSchema(thirdRows);
+ assertThat(thirdSchema.getField("v").type())
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.BOOLEAN(), DataTypes.BIGINT()},
+ new String[] {"emerging", "stable"})));
+ }
+
+ @Test
+ void testAdaptiveInferenceWidensScalarSelectedFromPriorEvidence() {
+ RowType schema =
+ RowType.of(
+ new DataType[] {DataTypes.VARIANT(), DataTypes.VARIANT()},
+ new String[] {"first", "second"});
+ VariantShreddingInferenceSession session =
+ new VariantShreddingInferenceSession(
+ new InferVariantShreddingSchema(schema, 6, 50, 0.1), 10, 0.1, 0.05);
+
+ RowType initialSchema =
+ session.inferSchema(
+ Arrays.asList(
+ GenericRow.of(
+ GenericVariant.fromJson("{\"a\":1,\"b\":2}"),
+ GenericVariant.fromJson("{\"historical\":12345}"))));
+ assertThat(initialSchema.getField("first").type())
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.BIGINT(), DataTypes.BIGINT()},
+ new String[] {"a", "b"})));
+ assertThat(initialSchema.getField("second").type())
+ .isEqualTo(variantShreddingSchema(DataTypes.VARIANT()));
+ session.commitPendingInference();
+
+ RowType adaptiveSchema =
+ session.inferSchema(
+ Arrays.asList(GenericRow.of(GenericVariant.fromJson("1"), null)));
+ assertThat(adaptiveSchema.getField("first").type())
+ .isEqualTo(variantShreddingSchema(DataTypes.BIGINT()));
+ assertThat(adaptiveSchema.getField("second").type())
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.BIGINT()},
+ new String[] {"historical"})));
+ }
+
@Test
void testInferSchemaWithDeepNesting() {
// Schema: row
diff --git a/paimon-common/src/test/java/org/apache/paimon/format/shredding/ShreddingFormatWriterTest.java b/paimon-common/src/test/java/org/apache/paimon/format/shredding/ShreddingFormatWriterTest.java
index 8345d2ae4137..0bba5e23d80a 100644
--- a/paimon-common/src/test/java/org/apache/paimon/format/shredding/ShreddingFormatWriterTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/format/shredding/ShreddingFormatWriterTest.java
@@ -28,6 +28,7 @@
import java.io.IOException;
import java.util.Collections;
+import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -44,6 +45,7 @@ void testCloseDelegateWhenCommitMetadataFails() {
delegate,
new ThrowingMetadataFactory(failure),
NoOpWritePlan.INSTANCE,
+ NoOpWritePlan.INSTANCE,
"none");
assertThatThrownBy(writer::close).isSameAs(failure);
@@ -90,7 +92,7 @@ public void commitShreddingMetadata(
}
}
- private enum NoOpWritePlan implements ShreddingWritePlan {
+ private enum NoOpWritePlan implements ShreddingWritePlan, ShreddingWritePlanFactory {
INSTANCE;
private final RowType rowType = new RowType(Collections.emptyList());
@@ -109,5 +111,25 @@ public RowType physicalRowType() {
public InternalRow toPhysicalRow(InternalRow row) {
return row;
}
+
+ @Override
+ public boolean shouldCreateWritePlan() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldInferWritePlan() {
+ return false;
+ }
+
+ @Override
+ public int inferBufferRowCount() {
+ return 0;
+ }
+
+ @Override
+ public ShreddingWritePlan createWritePlan(List sampleRows) {
+ return this;
+ }
}
}
diff --git a/paimon-core/src/main/java/org/apache/paimon/io/KeyValueFileWriterFactory.java b/paimon-core/src/main/java/org/apache/paimon/io/KeyValueFileWriterFactory.java
index 5644aab37512..7c55360fef4c 100644
--- a/paimon-core/src/main/java/org/apache/paimon/io/KeyValueFileWriterFactory.java
+++ b/paimon-core/src/main/java/org/apache/paimon/io/KeyValueFileWriterFactory.java
@@ -136,6 +136,7 @@ public DataFilePathFactory pathFactory(int level) {
public RollingFileWriter createRollingMergeTreeFileWriter(
int level, FileSource fileSource) {
WriteFormatKey key = new WriteFormatKey(level, false);
+ FormatWriterFactory writerFactory = formatContext.createWriterFactory(key);
// Row limit applies to writes only; compaction output stays size-only.
long targetFileRowNum =
fileSource == FileSource.COMPACT ? Long.MAX_VALUE : options.targetFileRowNum();
@@ -143,7 +144,11 @@ public RollingFileWriter createRollingMergeTreeFileWrite
() -> {
DataFilePathFactory pathFactory = formatContext.pathFactory(key);
return createDataFileWriter(
- pathFactory.newPath(), key, fileSource, pathFactory.isExternalPath());
+ pathFactory.newPath(),
+ key,
+ fileSource,
+ pathFactory.isExternalPath(),
+ writerFactory);
},
suggestedFileSize,
targetFileRowNum);
@@ -151,6 +156,7 @@ public RollingFileWriter createRollingMergeTreeFileWrite
public RollingFileWriter createRollingChangelogFileWriter(int level) {
WriteFormatKey key = new WriteFormatKey(level, true);
+ FormatWriterFactory writerFactory = formatContext.createWriterFactory(key);
return new RollingFileWriterImpl<>(
() -> {
DataFilePathFactory pathFactory = formatContext.pathFactory(key);
@@ -158,7 +164,8 @@ public RollingFileWriter createRollingChangelogFileWrite
pathFactory.newChangelogPath(),
key,
FileSource.APPEND,
- pathFactory.isExternalPath());
+ pathFactory.isExternalPath(),
+ writerFactory);
},
suggestedFileSize,
Long.MAX_VALUE);
@@ -166,21 +173,28 @@ public RollingFileWriter createRollingChangelogFileWrite
public RollingFileWriter createRollingClusteringFileWriter() {
WriteFormatKey key = new WriteFormatKey(1, false);
+ FormatWriterFactory writerFactory = formatContext.createWriterFactory(key);
return new RollingFileWriterImpl<>(
() -> {
DataFilePathFactory pathFactory = formatContext.pathFactory(key);
return createKvSeparatedFileWriter(
- pathFactory.newPath(), key, pathFactory.isExternalPath());
+ pathFactory.newPath(),
+ key,
+ pathFactory.isExternalPath(),
+ writerFactory);
},
suggestedFileSize,
Long.MAX_VALUE);
}
private KeyValueClusteringFileWriter createKvSeparatedFileWriter(
- Path path, WriteFormatKey key, boolean isExternalPath) {
+ Path path,
+ WriteFormatKey key,
+ boolean isExternalPath,
+ FormatWriterFactory writerFactory) {
return new KeyValueClusteringFileWriter(
fileIO,
- formatContext.fileWriterContext(key),
+ formatContext.fileWriterContext(key, writerFactory),
path,
keyType,
valueType,
@@ -193,7 +207,11 @@ private KeyValueClusteringFileWriter createKvSeparatedFileWriter(
}
private KeyValueDataFileWriter createDataFileWriter(
- Path path, WriteFormatKey key, FileSource fileSource, boolean isExternalPath) {
+ Path path,
+ WriteFormatKey key,
+ FileSource fileSource,
+ boolean isExternalPath,
+ FormatWriterFactory writerFactory) {
// Changelog is sequentially consumed, file index is unnecessary.
FileIndexOptions indexOptions = key.isChangelog ? new FileIndexOptions() : fileIndexOptions;
Set dataFileManagedBlobFields =
@@ -201,7 +219,7 @@ private KeyValueDataFileWriter createDataFileWriter(
return formatContext.thinModeEnabled
? new KeyValueThinDataFileWriterImpl(
fileIO,
- formatContext.fileWriterContext(key),
+ formatContext.fileWriterContext(key, writerFactory),
path,
new KeyValueThinSerializer(keyType, valueType)::toRow,
keyType,
@@ -215,7 +233,7 @@ private KeyValueDataFileWriter createDataFileWriter(
dataFileManagedBlobFields)
: new KeyValueDataFileWriterImpl(
fileIO,
- formatContext.fileWriterContext(key),
+ formatContext.fileWriterContext(key, writerFactory),
path,
new KeyValueSerializer(keyType, valueType)::toRow,
keyType,
@@ -322,7 +340,6 @@ private static class FileWriterContextFactory {
private final Map statsMode2AvroStats;
private final Map format2PathFactory;
private final Map formatFactory;
- private final Map format2WriterFactory;
private final BinaryRow partition;
private final int bucket;
@@ -388,7 +405,6 @@ private FileWriterContextFactory(
this.formatStats2Extractor = new HashMap<>();
this.statsMode2AvroStats = new HashMap<>();
this.format2PathFactory = new HashMap<>();
- this.format2WriterFactory = new HashMap<>();
this.formatFactory = new HashMap<>();
}
@@ -407,9 +423,10 @@ private boolean supportsThinMode(RowType keyType, RowType valueType) {
return true;
}
- private FileWriterContext fileWriterContext(WriteFormatKey key) {
+ private FileWriterContext fileWriterContext(
+ WriteFormatKey key, FormatWriterFactory writerFactory) {
return new FileWriterContext(
- writerFactory(key), statsProducer(key), key2Compress.apply(key));
+ writerFactory, statsProducer(key), key2Compress.apply(key));
}
private SimpleStatsProducer statsProducer(WriteFormatKey key) {
@@ -462,10 +479,8 @@ private DataFilePathFactory pathFactory(WriteFormatKey key) {
.createDataFilePathFactory(partition, bucket));
}
- private FormatWriterFactory writerFactory(WriteFormatKey key) {
- return format2WriterFactory.computeIfAbsent(
- key2Format.apply(key),
- format -> fileFormat(format).createWriterFactory(writeRowType));
+ private FormatWriterFactory createWriterFactory(WriteFormatKey key) {
+ return fileFormat(key2Format.apply(key)).createWriterFactory(writeRowType);
}
private FileFormat fileFormat(String format) {
diff --git a/paimon-core/src/main/java/org/apache/paimon/io/RowDataRollingFileWriter.java b/paimon-core/src/main/java/org/apache/paimon/io/RowDataRollingFileWriter.java
index e9874ef9b9e2..9b35c002aaf9 100644
--- a/paimon-core/src/main/java/org/apache/paimon/io/RowDataRollingFileWriter.java
+++ b/paimon-core/src/main/java/org/apache/paimon/io/RowDataRollingFileWriter.java
@@ -21,6 +21,7 @@
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.fileindex.FileIndexOptions;
import org.apache.paimon.format.FileFormat;
+import org.apache.paimon.format.FormatWriterFactory;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.manifest.FileSource;
@@ -54,28 +55,41 @@ public RowDataRollingFileWriter(
@Nullable FileFormat rowSidecarFormat,
long targetFileRowNum) {
super(
- () -> {
- Path dataPath = pathFactory.newPath();
- Path rowSidecarPath =
- rowSidecarFormat == null
- ? null
- : new Path(dataPath.getParent(), dataPath.getName() + ".row");
- return new RowDataFileWriter(
- fileIO,
- RollingFileWriter.createFileWriterContext(
- fileFormat, writeSchema, statsCollectors, fileCompression),
- dataPath,
- writeSchema,
- schemaId,
- seqNumCounterSupplier,
- fileIndexOptions,
- fileSource,
- asyncFileWrite,
- statsDenseStore,
- pathFactory.isExternalPath(),
- writeCols,
- rowSidecarFormat,
- rowSidecarPath);
+ new Supplier() {
+
+ private final FormatWriterFactory formatWriterFactory =
+ fileFormat.createWriterFactory(writeSchema);
+
+ @Override
+ public RowDataFileWriter get() {
+ Path dataPath = pathFactory.newPath();
+ Path rowSidecarPath =
+ rowSidecarFormat == null
+ ? null
+ : new Path(
+ dataPath.getParent(), dataPath.getName() + ".row");
+ FileWriterContext writerContext =
+ new FileWriterContext(
+ formatWriterFactory,
+ RollingFileWriter.createStatsProducer(
+ fileFormat, writeSchema, statsCollectors),
+ fileCompression);
+ return new RowDataFileWriter(
+ fileIO,
+ writerContext,
+ dataPath,
+ writeSchema,
+ schemaId,
+ seqNumCounterSupplier,
+ fileIndexOptions,
+ fileSource,
+ asyncFileWrite,
+ statsDenseStore,
+ pathFactory.isExternalPath(),
+ writeCols,
+ rowSidecarFormat,
+ rowSidecarPath);
+ }
},
targetFileSize,
targetFileRowNum);
diff --git a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
index 1c6ba3a0fb07..cdd2555c361c 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
@@ -367,6 +367,7 @@ public static void validateTableSchema(TableSchema schema, Set dynamicOp
validateMergeFunctionFactory(schema);
validateMapStorageLayout(schema, options);
+ validateVariantShreddingInferenceOptions(options);
validateFileIndex(schema);
@@ -695,6 +696,44 @@ private static void validateMapStorageLayout(TableSchema schema, CoreOptions opt
}
}
+ private static void validateVariantShreddingInferenceOptions(CoreOptions options) {
+ int coldSampleRows =
+ options.toConfiguration().get(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW);
+ checkArgument(
+ coldSampleRows > 0,
+ "%s must be positive.",
+ CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW.key());
+
+ double admissionRatio =
+ options.toConfiguration()
+ .get(CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO);
+ checkArgument(
+ admissionRatio >= 0 && admissionRatio <= 1,
+ "%s must be between 0 and 1.",
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key());
+
+ if (options.toConfiguration().get(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE)
+ != CoreOptions.VariantShreddingInferenceMode.ADAPTIVE) {
+ return;
+ }
+
+ int warmSampleRows =
+ options.toConfiguration()
+ .get(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW);
+ checkArgument(
+ warmSampleRows > 0,
+ "%s must be positive.",
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key());
+ double retentionRatio =
+ options.toConfiguration()
+ .get(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO);
+ checkArgument(
+ retentionRatio >= 0 && retentionRatio <= admissionRatio,
+ "%s must be between 0 and %s.",
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(),
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key());
+ }
+
private static void validateUnsupportedTypesWithMapSharedShredding(
TableSchema schema, CoreOptions options) {
RowType rowType = new RowType(schema.fields());
diff --git a/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java b/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
index 45f2a8a4b15f..ef567324473f 100644
--- a/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/append/AppendOnlyWriterTest.java
@@ -509,7 +509,7 @@ public void testWriteSharedShreddingWithFileIndex(String fileFormat) throws Exce
nameToId("a", 0, "b", 1),
fieldToColumns(0, columns(0), 1, columns(0)),
overflowFields(),
- 1,
+ 3,
1));
}
@@ -528,7 +528,7 @@ public void testSharedShreddingMapAllEmptyFirstFile(String fileFormat) throws Ex
assertThat(readSharedShreddingFieldMeta(context, file, "tags"))
.isEqualTo(
- sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 1, 0));
+ sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 3, 0));
}
@ParameterizedTest(name = "{0}")
@@ -557,17 +557,18 @@ public void testSharedShreddingMapAllNullThenAllEmptyFiles(String fileFormat) th
assertThat(readSharedShreddingFieldMeta(context, nullFile, "tags"))
.isEqualTo(
- sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 1, 0));
+ sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 3, 0));
assertThat(readSharedShreddingFieldMeta(context, emptyFile, "tags"))
.isEqualTo(
- sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 1, 0));
+ sharedShreddingMeta(nameToId(), fieldToColumns(), overflowFields(), 3, 0));
assertThat(readSharedShreddingFieldMeta(context, valuesFile, "tags"))
.isEqualTo(
sharedShreddingMeta(
nameToId("a", 0, "b", 1, "c", 2, "d", 3),
- fieldToColumns(0, columns(0), 1, columns(0), 2, columns(0)),
- overflowFields(3),
- 1,
+ fieldToColumns(
+ 0, columns(0), 1, columns(0), 2, columns(0), 3, columns(1)),
+ overflowFields(),
+ 3,
2));
}
@@ -613,9 +614,9 @@ public void testSharedShreddingMapDataFileMetaInfo(String fileFormat) throws Exc
.isEqualTo(
sharedShreddingMeta(
nameToId("a", 0, "b", 1, "c", 2),
- fieldToColumns(0, columns(0), 1, columns(1)),
- overflowFields(2),
- 2,
+ fieldToColumns(0, columns(0), 1, columns(1), 2, columns(2)),
+ overflowFields(),
+ 3,
3));
}
@@ -658,7 +659,7 @@ public void testSharedShreddingMapWithBlob(String fileFormat) throws Exception {
nameToId("a", 0),
fieldToColumns(0, columns(0)),
overflowFields(),
- 1,
+ 3,
1));
}
diff --git a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
index ab94f33b3a63..36a085c6a819 100644
--- a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
@@ -120,6 +120,83 @@ public void testTargetFileRowNumMustBePositive() {
.hasMessageContaining("target-file-row-num should be at least 1");
}
+ @Test
+ public void testVariantShreddingInferenceOptions() {
+ Map invalidColdSampleOptions = new HashMap<>();
+ invalidColdSampleOptions.put(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW.key(), "0");
+ assertThatThrownBy(() -> validateTableSchemaExec(invalidColdSampleOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW.key()
+ + " must be positive");
+
+ Map invalidAdmissionOptions = new HashMap<>();
+ invalidAdmissionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key(), "1.1");
+ assertThatThrownBy(() -> validateTableSchemaExec(invalidAdmissionOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key()
+ + " must be between 0 and 1");
+
+ Map negativeAdmissionOptions = new HashMap<>();
+ negativeAdmissionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key(), "-0.1");
+ assertThatThrownBy(() -> validateTableSchemaExec(negativeAdmissionOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key()
+ + " must be between 0 and 1");
+
+ Map invalidWarmSampleOptions = new HashMap<>();
+ invalidWarmSampleOptions.put(
+ CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "adaptive");
+ invalidWarmSampleOptions.put(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key(), "0");
+ assertThatThrownBy(() -> validateTableSchemaExec(invalidWarmSampleOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key()
+ + " must be positive");
+
+ Map invalidRetentionOptions = new HashMap<>();
+ invalidRetentionOptions.put(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "adaptive");
+ invalidRetentionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key(), "0.2");
+ invalidRetentionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(), "0.3");
+ assertThatThrownBy(() -> validateTableSchemaExec(invalidRetentionOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key()
+ + " must be between 0 and "
+ + CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key());
+
+ Map negativeRetentionOptions = new HashMap<>();
+ negativeRetentionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "adaptive");
+ negativeRetentionOptions.put(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(), "-0.1");
+ assertThatThrownBy(() -> validateTableSchemaExec(negativeRetentionOptions))
+ .hasMessageContaining(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key()
+ + " must be between 0 and "
+ + CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key());
+
+ Map perFileOptions = new HashMap<>();
+ perFileOptions.put(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "per-file");
+ perFileOptions.put(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key(), "0");
+ perFileOptions.put(CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(), "2");
+ assertThatCode(() -> validateTableSchemaExec(perFileOptions)).doesNotThrowAnyException();
+
+ Map validAdaptiveOptions = new HashMap<>();
+ validAdaptiveOptions.put(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "adaptive");
+ validAdaptiveOptions.put(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW.key(), "10");
+ validAdaptiveOptions.put(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key(), "2");
+ validAdaptiveOptions.put(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key(), "0.2");
+ validAdaptiveOptions.put(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(), "0.1");
+ assertThatCode(() -> validateTableSchemaExec(validAdaptiveOptions))
+ .doesNotThrowAnyException();
+ }
+
@Test
public void testFromSnapshotConflict() {
String timestampString =
diff --git a/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java b/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
index 666654231bc3..fe0614404414 100644
--- a/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
@@ -218,7 +218,7 @@ public void testAppendOnlyCompaction(String format) throws Exception {
assertThat(files.get(0).dataFile.fileSource()).hasValue(FileSource.COMPACT);
MapSharedShreddingFieldMeta compactedMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(0), "metrics");
- assertThat(compactedMeta.numColumns()).isEqualTo(2);
+ assertThat(compactedMeta.numColumns()).isEqualTo(64);
assertThat(compactedMeta.maxRowWidth()).isEqualTo(4);
assertThat(compactedMeta.nameToId()).containsOnlyKeys("a", "b", "c", "d", "e", "f", "g");
@@ -381,14 +381,21 @@ public void testAppendOnlyTableReadWriteWithAllSupportedComplexValueTypes(String
@ParameterizedTest
@ValueSource(strings = {"orc", "parquet"})
- public void testInferColumnCountFromFirstRowOfEachFile(String format) throws Exception {
+ public void testAdaptColumnCountAcrossRollingFiles(String format) throws Exception {
Table table = createTableWithBucket(format, 8, "1", "metrics");
+ catalog.alterTable(
+ identifier(format),
+ Collections.singletonList(
+ SchemaChange.setOption(CoreOptions.TARGET_FILE_ROW_NUM.key(), "2")),
+ false);
+ table = catalog.getTable(identifier(format));
write(
table,
GenericRow.of(1, mapOf("a", 11L, "b", 12L)),
- GenericRow.of(2, mapOf("c", 21L, "d", 22L, "e", 23L)));
- write(table, GenericRow.of(3, mapOf("f", 31L)));
+ GenericRow.of(2, mapOf("c", 21L, "d", 22L, "e", 23L)),
+ GenericRow.of(3, mapOf("f", 31L)),
+ GenericRow.of(4, mapOf("g", 41L, "h", 42L)));
FileStoreTable fileStoreTable = (FileStoreTable) table;
List files = currentDataFiles(fileStoreTable);
@@ -397,13 +404,13 @@ public void testInferColumnCountFromFirstRowOfEachFile(String format) throws Exc
MapSharedShreddingFieldMeta firstFileMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(0), "metrics");
- assertThat(firstFileMeta.numColumns()).isEqualTo(2);
+ assertThat(firstFileMeta.numColumns()).isEqualTo(8);
assertThat(firstFileMeta.maxRowWidth()).isEqualTo(3);
MapSharedShreddingFieldMeta secondFileMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(1), "metrics");
- assertThat(secondFileMeta.numColumns()).isEqualTo(1);
- assertThat(secondFileMeta.maxRowWidth()).isEqualTo(1);
+ assertThat(secondFileMeta.numColumns()).isEqualTo(3);
+ assertThat(secondFileMeta.maxRowWidth()).isEqualTo(2);
Map> actual = new LinkedHashMap<>();
for (InternalRow row : read(table)) {
@@ -413,7 +420,8 @@ public void testInferColumnCountFromFirstRowOfEachFile(String format) throws Exc
assertThat(actual)
.containsEntry(1, javaMapOf("a", 11L, "b", 12L))
.containsEntry(2, javaMapOf("c", 21L, "d", 22L, "e", 23L))
- .containsEntry(3, javaMapOf("f", 31L));
+ .containsEntry(3, javaMapOf("f", 31L))
+ .containsEntry(4, javaMapOf("g", 41L, "h", 42L));
}
@ParameterizedTest
@@ -690,8 +698,8 @@ public void testPrimaryKeyDeletionVectorCompaction(String format) throws Excepti
@ParameterizedTest
@CsvSource({"orc,false", "orc,true", "parquet,false", "parquet,true"})
- public void testPrimaryKeyInfersColumnCountPerFile(String format, boolean thinMode)
- throws Exception {
+ public void testPrimaryKeyStartsNewRollingWriterWithMaxColumnCount(
+ String format, boolean thinMode) throws Exception {
Table table = createPrimaryKeyTable(format, thinMode, 8);
write(table, GenericRow.of(1, mapOf("a", 11L, "b", 12L)));
@@ -704,12 +712,12 @@ public void testPrimaryKeyInfersColumnCountPerFile(String format, boolean thinMo
MapSharedShreddingFieldMeta firstFileMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(0), "metrics");
- assertThat(firstFileMeta.numColumns()).isEqualTo(2);
+ assertThat(firstFileMeta.numColumns()).isEqualTo(8);
assertThat(firstFileMeta.maxRowWidth()).isEqualTo(2);
MapSharedShreddingFieldMeta secondFileMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(1), "metrics");
- assertThat(secondFileMeta.numColumns()).isEqualTo(1);
+ assertThat(secondFileMeta.numColumns()).isEqualTo(8);
assertThat(secondFileMeta.maxRowWidth()).isEqualTo(1);
Map> actual = new LinkedHashMap<>();
@@ -754,7 +762,7 @@ public void testSwitchMapLayoutAndInferColumns(String format) throws Exception {
MapSharedShreddingFieldMeta metricsMeta =
readSharedShreddingFieldMeta(fileStoreTable, files.get(1), "metrics");
- assertThat(metricsMeta.numColumns()).isEqualTo(1);
+ assertThat(metricsMeta.numColumns()).isEqualTo(3);
assertThat(metricsMeta.maxRowWidth()).isEqualTo(1);
Map>> actual = new LinkedHashMap<>();
diff --git a/paimon-format/src/test/java/org/apache/paimon/format/parquet/writer/InferVariantShreddingWriteTest.java b/paimon-format/src/test/java/org/apache/paimon/format/parquet/writer/InferVariantShreddingWriteTest.java
index 3e6b30df2f71..2d2541361659 100644
--- a/paimon-format/src/test/java/org/apache/paimon/format/parquet/writer/InferVariantShreddingWriteTest.java
+++ b/paimon-format/src/test/java/org/apache/paimon/format/parquet/writer/InferVariantShreddingWriteTest.java
@@ -121,6 +121,243 @@ public void testInferSchemaWithSimpleObject() throws Exception {
assertThat(result2.get(2)).isEqualTo(GenericRow.of(GenericRow.of(35)));
}
+ @Test
+ public void testAdaptiveInferenceAcrossFiles() throws Exception {
+ ParquetFileFormat format = createFormat(adaptiveOptions(10, 10, 0.4, 0.2));
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "payload", DataTypes.VARIANT()));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+
+ List firstRows = new ArrayList<>();
+ List firstJson = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ String json =
+ i < 5
+ ? "{\"legacy\":\"value\",\"stable\":" + i + "}"
+ : "{\"stable\":" + i + "}";
+ firstRows.add(GenericRow.of(GenericVariant.fromJson(json)));
+ firstJson.add(json);
+ }
+ Path firstFile = file;
+ writeRows(factory, firstFile, firstRows.toArray(new InternalRow[0]));
+
+ List secondRows = new ArrayList<>();
+ List secondJson = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ String json =
+ i < 9 ? "{\"emerging\":true,\"stable\":" + i + "}" : "{\"stable\":" + i + "}";
+ secondRows.add(GenericRow.of(GenericVariant.fromJson(json)));
+ secondJson.add(json);
+ }
+ Path secondFile = newFile();
+ writeRows(factory, secondFile, secondRows.toArray(new InternalRow[0]));
+
+ List thirdRows = new ArrayList<>();
+ List thirdJson = new ArrayList<>();
+ for (int i = 0; i < 10; i++) {
+ String json = "{\"stable\":" + i + "}";
+ thirdRows.add(GenericRow.of(GenericVariant.fromJson(json)));
+ thirdJson.add(json);
+ }
+ Path thirdFile = newFile();
+ writeRows(factory, thirdFile, thirdRows.toArray(new InternalRow[0]));
+
+ assertThat(readVariantFileType(firstFile, "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.STRING(), DataTypes.BIGINT()},
+ new String[] {"legacy", "stable"})));
+ assertThat(readVariantFileType(secondFile, "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {
+ DataTypes.BOOLEAN(),
+ DataTypes.STRING(),
+ DataTypes.BIGINT()
+ },
+ new String[] {"emerging", "legacy", "stable"})));
+ assertThat(readVariantFileType(thirdFile, "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.BOOLEAN(), DataTypes.BIGINT()},
+ new String[] {"emerging", "stable"})));
+
+ assertThat(readVariantJson(format, writeType, firstFile, 0))
+ .containsExactlyElementsOf(firstJson);
+ assertThat(readVariantJson(format, writeType, secondFile, 0))
+ .containsExactlyElementsOf(secondJson);
+ assertThat(readVariantJson(format, writeType, thirdFile, 0))
+ .containsExactlyElementsOf(thirdJson);
+ }
+
+ @Test
+ public void testAdaptiveInferenceOnShortRolledFile() throws Exception {
+ ParquetFileFormat format = createFormat(adaptiveOptions(4, 4, 0.4, 0.2));
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "payload", DataTypes.VARIANT()));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+
+ Path firstFile = file;
+ writeRows(
+ factory,
+ firstFile,
+ GenericRow.of(GenericVariant.fromJson("{\"legacy\":\"a\",\"stable\":1}")),
+ GenericRow.of(GenericVariant.fromJson("{\"legacy\":\"b\",\"stable\":2}")),
+ GenericRow.of(GenericVariant.fromJson("{\"legacy\":\"c\",\"stable\":3}")),
+ GenericRow.of(GenericVariant.fromJson("{\"legacy\":\"d\",\"stable\":4}")));
+
+ Path secondFile = newFile();
+ writeRows(
+ factory,
+ secondFile,
+ GenericRow.of(GenericVariant.fromJson("{\"emerging\":true,\"stable\":5}")),
+ GenericRow.of(GenericVariant.fromJson("{\"emerging\":false,\"stable\":6}")),
+ GenericRow.of(GenericVariant.fromJson("{\"emerging\":true,\"stable\":7}")));
+
+ assertThat(readVariantFileType(secondFile, "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {
+ DataTypes.BOOLEAN(),
+ DataTypes.STRING(),
+ DataTypes.BIGINT()
+ },
+ new String[] {"emerging", "legacy", "stable"})));
+ assertThat(readVariantJson(format, writeType, secondFile, 0))
+ .containsExactly(
+ "{\"emerging\":true,\"stable\":5}",
+ "{\"emerging\":false,\"stable\":6}",
+ "{\"emerging\":true,\"stable\":7}");
+ }
+
+ @Test
+ public void testAdaptiveInferenceWithMultipleVariantFields() throws Exception {
+ ParquetFileFormat format = createFormat(adaptiveOptions(2, 2, 0.4, 0.2));
+ RowType writeType =
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "left_payload", DataTypes.VARIANT()),
+ DataTypes.FIELD(1, "right_payload", DataTypes.VARIANT()));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+
+ Path firstFile = file;
+ writeRows(
+ factory,
+ firstFile,
+ GenericRow.of(
+ GenericVariant.fromJson("{\"legacy\":\"a\",\"stable\":1}"),
+ GenericVariant.fromJson("{\"sparse\":\"x\",\"stable\":\"a\"}")),
+ GenericRow.of(
+ GenericVariant.fromJson("{\"legacy\":\"b\",\"stable\":2}"),
+ GenericVariant.fromJson("{\"stable\":\"b\"}")));
+
+ Path secondFile = newFile();
+ writeRows(
+ factory,
+ secondFile,
+ GenericRow.of(
+ GenericVariant.fromJson("{\"emerging\":true,\"stable\":3}"),
+ GenericVariant.fromJson("{\"stable\":\"c\"}")),
+ GenericRow.of(
+ GenericVariant.fromJson("{\"emerging\":false,\"stable\":4}"),
+ GenericVariant.fromJson("{\"emerging\":true,\"stable\":\"d\"}")));
+
+ assertThat(readVariantFileType(firstFile, "left_payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.STRING(), DataTypes.BIGINT()},
+ new String[] {"legacy", "stable"})));
+ assertThat(readVariantFileType(secondFile, "left_payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {
+ DataTypes.BOOLEAN(),
+ DataTypes.STRING(),
+ DataTypes.BIGINT()
+ },
+ new String[] {"emerging", "legacy", "stable"})));
+
+ RowType rightSchema =
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.STRING(), DataTypes.STRING()},
+ new String[] {"sparse", "stable"}));
+ assertThat(readVariantFileType(firstFile, "right_payload")).isEqualTo(rightSchema);
+ assertThat(readVariantFileType(secondFile, "right_payload")).isEqualTo(rightSchema);
+
+ List result = readRows(format, writeType, secondFile);
+ assertThat(result.get(0).getVariant(0).toJson())
+ .isEqualTo("{\"emerging\":true,\"stable\":3}");
+ assertThat(result.get(0).getVariant(1).toJson()).isEqualTo("{\"stable\":\"c\"}");
+ assertThat(result.get(1).getVariant(0).toJson())
+ .isEqualTo("{\"emerging\":false,\"stable\":4}");
+ assertThat(result.get(1).getVariant(1).toJson())
+ .isEqualTo("{\"emerging\":true,\"stable\":\"d\"}");
+ }
+
+ @Test
+ public void testAdaptiveInferenceWithNestedVariant() throws Exception {
+ RowType nestedType =
+ DataTypes.ROW(
+ DataTypes.FIELD(1, "label", DataTypes.STRING()),
+ DataTypes.FIELD(2, "payload", DataTypes.VARIANT()));
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "nested", nestedType));
+ ParquetFileFormat format = createFormat(adaptiveOptions(2, 2, 0.4, 0.2));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+
+ Path firstFile = file;
+ writeRows(
+ factory,
+ firstFile,
+ GenericRow.of(
+ GenericRow.of(
+ BinaryString.fromString("first"),
+ GenericVariant.fromJson("{\"legacy\":\"a\",\"stable\":1}"))),
+ GenericRow.of(
+ GenericRow.of(
+ BinaryString.fromString("second"),
+ GenericVariant.fromJson("{\"legacy\":\"b\",\"stable\":2}"))));
+
+ Path secondFile = newFile();
+ writeRows(
+ factory,
+ secondFile,
+ GenericRow.of(
+ GenericRow.of(
+ BinaryString.fromString("third"),
+ GenericVariant.fromJson("{\"emerging\":true,\"stable\":3}"))),
+ GenericRow.of(
+ GenericRow.of(
+ BinaryString.fromString("fourth"),
+ GenericVariant.fromJson("{\"emerging\":false,\"stable\":4}"))));
+
+ assertThat(readVariantFileType(firstFile, "nested", "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {DataTypes.STRING(), DataTypes.BIGINT()},
+ new String[] {"legacy", "stable"})));
+ assertThat(readVariantFileType(secondFile, "nested", "payload"))
+ .isEqualTo(
+ variantShreddingSchema(
+ RowType.of(
+ new DataType[] {
+ DataTypes.BOOLEAN(),
+ DataTypes.STRING(),
+ DataTypes.BIGINT()
+ },
+ new String[] {"emerging", "legacy", "stable"})));
+
+ List result = readRows(format, writeType, secondFile);
+ assertThat(result.get(0).getRow(0, 2).getVariant(1).toJson())
+ .isEqualTo("{\"emerging\":true,\"stable\":3}");
+ assertThat(result.get(1).getRow(0, 2).getVariant(1).toJson())
+ .isEqualTo("{\"emerging\":false,\"stable\":4}");
+ }
+
@Test
public void testInferSchemaWithArray() throws Exception {
ParquetFileFormat format = createFormat();
@@ -513,26 +750,73 @@ protected ParquetFileFormat createFormat(Options options) {
return new ParquetFileFormat(new FileFormatFactory.FormatContext(options, 1024, 1024));
}
+ private Options adaptiveOptions(
+ int initialSampleRows,
+ int adaptiveSampleRows,
+ double admissionRatio,
+ double retentionRatio) {
+ Options options = defaultOptions();
+ options.set(CoreOptions.VARIANT_SHREDDING_INFERENCE_MODE.key(), "adaptive");
+ options.set(
+ CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW.key(),
+ String.valueOf(initialSampleRows));
+ options.set(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_MAX_INFER_BUFFER_ROW.key(),
+ String.valueOf(adaptiveSampleRows));
+ options.set(
+ CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO.key(),
+ String.valueOf(admissionRatio));
+ options.set(
+ CoreOptions.VARIANT_SHREDDING_ADAPTIVE_RETENTION_RATIO.key(),
+ String.valueOf(retentionRatio));
+ return options;
+ }
+
+ private Path newFile() {
+ return new Path(parent, UUID.randomUUID() + ".parquet");
+ }
+
protected List readRows(ParquetFileFormat format, RowType rowType)
throws IOException {
+ return readRows(format, rowType, file);
+ }
+
+ private List readRows(ParquetFileFormat format, RowType rowType, Path dataFile)
+ throws IOException {
List result = new ArrayList<>();
try (RecordReader reader =
format.createReaderFactory(rowType, rowType, new ArrayList<>())
.createReader(
- new FormatReaderContext(fileIO, file, fileIO.getFileSize(file)))) {
+ new FormatReaderContext(
+ fileIO, dataFile, fileIO.getFileSize(dataFile)))) {
InternalRowSerializer serializer = new InternalRowSerializer(rowType);
reader.forEachRemaining(row -> result.add(serializer.copy(row)));
}
return result;
}
+ private List readVariantJson(
+ ParquetFileFormat format, RowType rowType, Path dataFile, int fieldIndex)
+ throws IOException {
+ List result = new ArrayList<>();
+ for (InternalRow row : readRows(format, rowType, dataFile)) {
+ result.add(row.getVariant(fieldIndex).toJson());
+ }
+ return result;
+ }
+
protected void writeRows(FormatWriterFactory factory, InternalRow... rows) throws IOException {
+ writeRows(factory, file, rows);
+ }
+
+ private void writeRows(FormatWriterFactory factory, Path dataFile, InternalRow... rows)
+ throws IOException {
FormatWriter writer;
PositionOutputStream out = null;
if (factory instanceof SupportsDirectWrite) {
- writer = ((SupportsDirectWrite) factory).create(fileIO, file, "zstd");
+ writer = ((SupportsDirectWrite) factory).create(fileIO, dataFile, "zstd");
} else {
- out = fileIO.newOutputStream(file, false);
+ out = fileIO.newOutputStream(dataFile, false);
writer = factory.create(out, "zstd");
}
for (InternalRow row : rows) {
@@ -544,6 +828,19 @@ protected void writeRows(FormatWriterFactory factory, InternalRow... rows) throw
}
}
+ private RowType readVariantFileType(Path dataFile, String... fieldPath) throws IOException {
+ try (ParquetFileReader reader =
+ ParquetUtil.getParquetReader(
+ fileIO, dataFile, fileIO.getFileSize(dataFile), new Options())) {
+ Type variantType = reader.getFooter().getFileMetaData().getSchema();
+ for (String fieldName : fieldPath) {
+ variantType = variantType.asGroupType().getType(fieldName);
+ }
+ return VariantMetadataUtils.addVariantMetadata(
+ VariantShreddingReadPlanFactory.variantFileType(variantType));
+ }
+ }
+
protected void verifyShreddingSchema(RowType... expectShreddedTypes) throws IOException {
try (ParquetFileReader reader =
ParquetUtil.getParquetReader(