This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new eea31a30ac [core] Refactor shredding write plan framework (#8379)
eea31a30ac is described below

commit eea31a30ac78a713c749d58df5e80fbee808eda3
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Jun 30 10:47:53 2026 +0800

    [core] Refactor shredding write plan framework (#8379)
    
    Refactor Variant shredding writes onto a generic per-file shredding
    write plan framework, and prepare the same framework for MAP
    shared-shredding writes.
---
 .../data/shredding/MapSharedShreddingUtils.java    |  21 +-
 .../shredding/MapSharedShreddingWritePlan.java     |  85 ++++++++
 .../MapSharedShreddingWritePlanFactory.java        |  63 ++++++
 .../paimon/data/shredding/ShreddingWritePlan.java  |  51 +++++
 .../data/variant/VariantShreddingWritePlan.java    | 221 +++++++++++++++++++++
 .../InferShreddingWritePlanWriter.java}            | 141 ++++++-------
 .../format/shredding/ShreddingFormatWriter.java    | 119 +++++++++++
 .../shredding/ShreddingWritePlanFactory.java       |  39 ++++
 .../ShreddingWritePlanWriterFactories.java         |  49 +++++
 .../shredding/ShreddingWritePlanWriterFactory.java | 107 ++++++++++
 .../SupportsShreddingWritePlan.java}               |  31 +--
 .../variant/VariantInferenceWriterFactory.java     |  64 ------
 ....java => VariantShreddingWritePlanFactory.java} |  78 ++++++--
 .../shredding/MapSharedShreddingUtilsTest.java     |  37 ++--
 .../shredding/MapSharedShreddingWritePlanTest.java | 102 ++++++++++
 .../variant/VariantShreddingWritePlanTest.java     | 104 ++++++++++
 .../shredding/ShreddingFormatWriterTest.java       | 113 +++++++++++
 .../apache/paimon/format/orc/OrcFileFormat.java    |   7 +-
 .../apache/paimon/format/orc/OrcWriterFactory.java |  76 +++++--
 .../paimon/format/parquet/ParquetFileFormat.java   |   9 +-
 .../format/parquet/ParquetWriterFactory.java       |  33 ++-
 .../apache/paimon/format/parquet/VariantUtils.java |  43 ----
 .../parquet/writer/ParquetRowDataBuilder.java      |  24 +--
 .../parquet/writer/ParquetRowDataWriter.java       |  45 +----
 .../parquet/writer/RowDataParquetBuilder.java      |  17 +-
 .../reader/FileTypeNotMatchReadTypeTest.java       |   8 +-
 .../reader/ParquetRowDataBuilderForTest.java       |   4 +-
 .../writer/InferVariantShreddingWriteTest.java     |   4 +-
 .../shredding/ShreddingWritePlanFormatTest.java    | 161 +++++++++++++++
 .../apache/paimon/spark/sql/VariantTestBase.scala  |  21 +-
 30 files changed, 1518 insertions(+), 359 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingUtils.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingUtils.java
index 6f0cd879c5..851edb01d8 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingUtils.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingUtils.java
@@ -113,7 +113,7 @@ public class MapSharedShreddingUtils {
 
     public static void serializeMetadata(
             MapSharedShreddingFieldMeta fieldMeta,
-            String compression,
+            @Nullable String compression,
             Map<String, String> metadata) {
         metadata.put(
                 MapShreddingDefine.STORAGE_LAYOUT,
@@ -142,7 +142,7 @@ public class MapSharedShreddingUtils {
     }
 
     public static MapSharedShreddingFieldMeta deserializeMetadata(
-            @Nullable Map<String, String> metadata, String compression) {
+            @Nullable Map<String, String> metadata, @Nullable String 
compression) {
         if (!hasShreddingMetadata(metadata)) {
             throw new IllegalArgumentException(
                     "metadata is null or storage layout is not 
shared-shredding");
@@ -235,7 +235,11 @@ public class MapSharedShreddingUtils {
         }
     }
 
-    private static byte[] compress(byte[] input, String compression) {
+    private static byte[] compress(byte[] input, @Nullable String compression) 
{
+        if (isNoCompression(compression)) {
+            return input;
+        }
+
         BlockCompressionFactory factory =
                 BlockCompressionFactory.create(new 
CompressOptions(compression, 1));
         if (factory == null) {
@@ -247,7 +251,12 @@ public class MapSharedShreddingUtils {
         return Arrays.copyOf(output, actualSize);
     }
 
-    private static byte[] decompress(byte[] input, int originalLength, String 
compression) {
+    private static byte[] decompress(
+            byte[] input, int originalLength, @Nullable String compression) {
+        if (isNoCompression(compression)) {
+            return input;
+        }
+
         BlockCompressionFactory factory =
                 BlockCompressionFactory.create(new 
CompressOptions(compression, 1));
         if (factory == null) {
@@ -259,6 +268,10 @@ public class MapSharedShreddingUtils {
         return Arrays.copyOf(output, actualSize);
     }
 
+    private static boolean isNoCompression(@Nullable String compression) {
+        return compression == null || "none".equalsIgnoreCase(compression);
+    }
+
     private static String bytesToString(byte[] bytes) {
         return new String(bytes, StandardCharsets.ISO_8859_1);
     }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlan.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlan.java
new file mode 100644
index 0000000000..0c84a33169
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlan.java
@@ -0,0 +1,85 @@
+/*
+ * 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.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.types.RowType;
+
+import javax.annotation.Nullable;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** A physical write plan for the shared-shredding MAP layout. */
+public class MapSharedShreddingWritePlan implements ShreddingWritePlan {
+
+    private final RowType logicalRowType;
+    private final MapSharedShreddingRowConverter converter;
+    @Nullable private final MapSharedShreddingContext context;
+
+    @Nullable private Map<String, Map<String, String>> fieldMetadata;
+
+    public MapSharedShreddingWritePlan(
+            RowType logicalRowType,
+            Map<String, Integer> fieldToNumColumns,
+            @Nullable MapSharedShreddingContext context) {
+        this.logicalRowType = logicalRowType;
+        this.converter = new MapSharedShreddingRowConverter(logicalRowType, 
fieldToNumColumns);
+        this.context = context;
+    }
+
+    @Override
+    public RowType logicalRowType() {
+        return logicalRowType;
+    }
+
+    @Override
+    public RowType physicalRowType() {
+        return converter.physicalType();
+    }
+
+    @Override
+    public InternalRow toPhysicalRow(InternalRow row) {
+        return converter.convert(row);
+    }
+
+    @Override
+    public Map<String, Map<String, String>> fieldMetadata(String compression) {
+        if (fieldMetadata == null) {
+            fieldMetadata = buildFieldMetadata(compression == null ? "none" : 
compression);
+        }
+        return fieldMetadata;
+    }
+
+    private Map<String, Map<String, String>> buildFieldMetadata(String 
compression) {
+        Map<String, Map<String, String>> metadata = new LinkedHashMap<>();
+        for (String fieldName : converter.shreddingFieldNames()) {
+            MapSharedShreddingFieldMeta fieldMeta = 
converter.buildFieldMeta(fieldName);
+            Map<String, String> fieldMetadata = new LinkedHashMap<>();
+            MapSharedShreddingUtils.serializeMetadata(fieldMeta, compression, 
fieldMetadata);
+            metadata.put(fieldName, 
Collections.unmodifiableMap(fieldMetadata));
+
+            if (context != null) {
+                context.reportFileStats(fieldName, fieldMeta.maxRowWidth());
+            }
+        }
+        return Collections.unmodifiableMap(metadata);
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanFactory.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanFactory.java
new file mode 100644
index 0000000000..6e500f1e91
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanFactory.java
@@ -0,0 +1,63 @@
+/*
+ * 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.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.format.shredding.ShreddingWritePlanFactory;
+import org.apache.paimon.types.RowType;
+
+import java.util.List;
+
+/** Creates per-file shared-shredding MAP write plans. */
+public class MapSharedShreddingWritePlanFactory implements 
ShreddingWritePlanFactory {
+
+    private final RowType logicalRowType;
+    private final MapSharedShreddingContext context;
+
+    public MapSharedShreddingWritePlanFactory(
+            RowType logicalRowType, MapSharedShreddingContext context) {
+        this.logicalRowType = logicalRowType;
+        this.context = context;
+    }
+
+    @Override
+    public RowType logicalRowType() {
+        return logicalRowType;
+    }
+
+    @Override
+    public boolean shouldCreateWritePlan() {
+        return !context.isEmpty();
+    }
+
+    @Override
+    public boolean shouldInferWritePlan() {
+        return false;
+    }
+
+    @Override
+    public int inferBufferRowCount() {
+        return 0;
+    }
+
+    @Override
+    public ShreddingWritePlan createWritePlan(List<InternalRow> sampleRows) {
+        return new MapSharedShreddingWritePlan(logicalRowType, 
context.computeNextK(), context);
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/ShreddingWritePlan.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/ShreddingWritePlan.java
new file mode 100644
index 0000000000..ac94be1964
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/ShreddingWritePlan.java
@@ -0,0 +1,51 @@
+/*
+ * 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.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.types.RowType;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A per-file physical layout plan for shredded fields.
+ *
+ * <p>The logical row type is the table-visible schema. The physical row type 
is the schema written
+ * into the data file. Implementations convert logical rows to physical rows 
before handing them to
+ * a file format writer.
+ */
+public interface ShreddingWritePlan {
+
+    RowType logicalRowType();
+
+    RowType physicalRowType();
+
+    InternalRow toPhysicalRow(InternalRow row);
+
+    /**
+     * Returns top-level field metadata to persist with the physical row type.
+     *
+     * <p>The outer map is keyed by physical top-level field name. The inner 
map contains metadata
+     * for that field. Formats decide how to encode this metadata into the 
actual file footer.
+     */
+    default Map<String, Map<String, String>> fieldMetadata(String compression) 
{
+        return Collections.emptyMap();
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingWritePlan.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingWritePlan.java
new file mode 100644
index 0000000000..27ffa8247a
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantShreddingWritePlan.java
@@ -0,0 +1,221 @@
+/*
+ * 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.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.VariantType;
+import org.apache.paimon.utils.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A physical write plan for Variant shredding. */
+public class VariantShreddingWritePlan implements ShreddingWritePlan {
+
+    private final RowType logicalRowType;
+    private final RowType physicalRowType;
+    private final RowConverter rowConverter;
+
+    public VariantShreddingWritePlan(RowType logicalRowType, RowType 
physicalRowType) {
+        this.logicalRowType = logicalRowType;
+        this.physicalRowType = physicalRowType;
+        this.rowConverter = RowConverter.create(logicalRowType, 
physicalRowType);
+    }
+
+    public static VariantShreddingWritePlan fromConfiguredSchema(
+            RowType logicalRowType, RowType configuredShreddingSchema) {
+        return new VariantShreddingWritePlan(
+                logicalRowType,
+                configuredPhysicalRowType(logicalRowType, 
configuredShreddingSchema));
+    }
+
+    @Override
+    public RowType logicalRowType() {
+        return logicalRowType;
+    }
+
+    @Override
+    public RowType physicalRowType() {
+        return physicalRowType;
+    }
+
+    @Override
+    public InternalRow toPhysicalRow(InternalRow row) {
+        return rowConverter.convert(row);
+    }
+
+    private static RowType configuredPhysicalRowType(
+            RowType logicalRowType, RowType configuredShreddingSchema) {
+        List<DataField> fields = new ArrayList<>();
+        for (DataField field : logicalRowType.getFields()) {
+            if (field.type() instanceof VariantType
+                    && configuredShreddingSchema.containsField(field.name())) {
+                RowType fieldShreddingSchema =
+                        PaimonShreddingUtils.variantShreddingSchema(
+                                
configuredShreddingSchema.getField(field.name()).type());
+                fields.add(field.newType(fieldShreddingSchema));
+            } else {
+                fields.add(field);
+            }
+        }
+        return new RowType(logicalRowType.isNullable(), fields);
+    }
+
+    private interface FieldConverter {
+
+        Object convert(InternalRow row);
+
+        boolean isIdentity();
+    }
+
+    private static class RowConverter {
+
+        private final FieldConverter[] fieldConverters;
+        private final boolean identity;
+
+        private RowConverter(FieldConverter[] fieldConverters, boolean 
identity) {
+            this.fieldConverters = fieldConverters;
+            this.identity = identity;
+        }
+
+        private static RowConverter create(RowType logicalRowType, RowType 
physicalRowType) {
+            Preconditions.checkArgument(
+                    logicalRowType.getFieldCount() == 
physicalRowType.getFieldCount(),
+                    "Logical and physical row types should have the same field 
count.");
+
+            FieldConverter[] converters = new 
FieldConverter[logicalRowType.getFieldCount()];
+            boolean identity = true;
+            for (int i = 0; i < converters.length; i++) {
+                DataType logicalType = logicalRowType.getTypeAt(i);
+                DataType physicalType = physicalRowType.getTypeAt(i);
+                converters[i] = createFieldConverter(i, logicalType, 
physicalType);
+                identity = identity && converters[i].isIdentity();
+            }
+            return new RowConverter(converters, identity);
+        }
+
+        private InternalRow convert(InternalRow row) {
+            if (identity) {
+                return row;
+            }
+
+            GenericRow physicalRow = new GenericRow(row.getRowKind(), 
fieldConverters.length);
+            for (int i = 0; i < fieldConverters.length; i++) {
+                physicalRow.setField(i, fieldConverters[i].convert(row));
+            }
+            return physicalRow;
+        }
+
+        private static FieldConverter createFieldConverter(
+                int fieldIndex, DataType logicalType, DataType physicalType) {
+            if (logicalType instanceof VariantType && physicalType instanceof 
RowType) {
+                return new VariantFieldConverter(
+                        fieldIndex,
+                        PaimonShreddingUtils.buildVariantSchema((RowType) 
physicalType));
+            }
+
+            if (logicalType instanceof RowType && physicalType instanceof 
RowType) {
+                RowConverter nestedConverter =
+                        RowConverter.create((RowType) logicalType, (RowType) 
physicalType);
+                if (!nestedConverter.identity) {
+                    return new RowFieldConverter(
+                            fieldIndex, (RowType) logicalType, 
nestedConverter);
+                }
+            }
+
+            return new IdentityFieldConverter(fieldIndex, logicalType);
+        }
+    }
+
+    private static class IdentityFieldConverter implements FieldConverter {
+
+        private final InternalRow.FieldGetter fieldGetter;
+
+        private IdentityFieldConverter(int fieldIndex, DataType logicalType) {
+            this.fieldGetter = InternalRow.createFieldGetter(logicalType, 
fieldIndex);
+        }
+
+        @Override
+        public Object convert(InternalRow row) {
+            return fieldGetter.getFieldOrNull(row);
+        }
+
+        @Override
+        public boolean isIdentity() {
+            return true;
+        }
+    }
+
+    private static class RowFieldConverter implements FieldConverter {
+
+        private final int fieldIndex;
+        private final int fieldCount;
+        private final RowConverter rowConverter;
+
+        private RowFieldConverter(
+                int fieldIndex, RowType logicalRowType, RowConverter 
rowConverter) {
+            this.fieldIndex = fieldIndex;
+            this.fieldCount = logicalRowType.getFieldCount();
+            this.rowConverter = rowConverter;
+        }
+
+        @Override
+        public Object convert(InternalRow row) {
+            if (row.isNullAt(fieldIndex)) {
+                return null;
+            }
+            return rowConverter.convert(row.getRow(fieldIndex, fieldCount));
+        }
+
+        @Override
+        public boolean isIdentity() {
+            return false;
+        }
+    }
+
+    private static class VariantFieldConverter implements FieldConverter {
+
+        private final int fieldIndex;
+        private final VariantSchema variantSchema;
+
+        private VariantFieldConverter(int fieldIndex, VariantSchema 
variantSchema) {
+            this.fieldIndex = fieldIndex;
+            this.variantSchema = variantSchema;
+        }
+
+        @Override
+        public Object convert(InternalRow row) {
+            if (row.isNullAt(fieldIndex)) {
+                return null;
+            }
+            return PaimonShreddingUtils.castShredded(
+                    (GenericVariant) row.getVariant(fieldIndex), 
variantSchema);
+        }
+
+        @Override
+        public boolean isIdentity() {
+            return false;
+        }
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/InferVariantShreddingWriter.java
 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
similarity index 51%
rename from 
paimon-common/src/main/java/org/apache/paimon/format/variant/InferVariantShreddingWriter.java
rename to 
paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
index 077a8d3da1..5f497d2d92 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/InferVariantShreddingWriter.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/InferShreddingWritePlanWriter.java
@@ -16,58 +16,46 @@
  * limitations under the License.
  */
 
-package org.apache.paimon.format.variant;
+package org.apache.paimon.format.shredding;
 
 import org.apache.paimon.data.InternalRow;
-import org.apache.paimon.data.variant.InferVariantShreddingSchema;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
 import org.apache.paimon.format.BundleFormatWriter;
 import org.apache.paimon.format.FormatWriter;
 import org.apache.paimon.fs.PositionOutputStream;
 import org.apache.paimon.io.BundleRecords;
-import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.InternalRowUtils;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
-/**
- * A generic writer that infers the shredding schema from buffered rows before 
writing.
- *
- * <p>This writer buffers rows up to a threshold, infers the optimal schema 
from them, then writes
- * all data using the inferred schema. It works with any format that 
implements {@link
- * SupportsVariantInference}.
- */
-public class InferVariantShreddingWriter implements BundleFormatWriter {
+/** Buffers initial rows, infers a per-file shredding write plan, and writes 
physical rows. */
+public class InferShreddingWritePlanWriter implements BundleFormatWriter {
 
-    private final SupportsVariantInference writerFactory;
-    private final RowType rowType;
-    private final InferVariantShreddingSchema shreddingSchemaInfer;
-    private final int maxBufferRow;
+    private final SupportsShreddingWritePlan writerFactory;
+    private final ShreddingWritePlanFactory writePlanFactory;
     private final PositionOutputStream out;
     private final String compression;
 
     private final List<InternalRow> bufferedRows;
     private final List<BundleRecords> bufferedBundles;
 
-    private FormatWriter actualWriter;
-    private boolean schemaFinalized = false;
+    @Nullable private FormatWriter actualWriter;
+    private boolean planFinalized = false;
     private long totalBufferedRowCount = 0;
 
-    public InferVariantShreddingWriter(
-            SupportsVariantInference writerFactory,
-            RowType rowType,
-            InferVariantShreddingSchema shreddingSchemaInfer,
-            int maxBufferRow,
+    public InferShreddingWritePlanWriter(
+            SupportsShreddingWritePlan writerFactory,
+            ShreddingWritePlanFactory writePlanFactory,
             PositionOutputStream out,
             String compression) {
         this.writerFactory = writerFactory;
-        this.rowType = rowType;
-        this.shreddingSchemaInfer = shreddingSchemaInfer;
-        this.maxBufferRow = maxBufferRow;
+        this.writePlanFactory = writePlanFactory;
         this.out = out;
         this.compression = compression;
         this.bufferedRows = new ArrayList<>();
@@ -76,58 +64,56 @@ public class InferVariantShreddingWriter implements 
BundleFormatWriter {
 
     @Override
     public void addElement(InternalRow row) throws IOException {
-        if (!schemaFinalized) {
-            bufferedRows.add(InternalRowUtils.copyInternalRow(row, rowType));
+        if (!planFinalized) {
+            bufferedRows.add(
+                    InternalRowUtils.copyInternalRow(row, 
writePlanFactory.logicalRowType()));
             totalBufferedRowCount++;
-            if (totalBufferedRowCount >= maxBufferRow) {
-                finalizeSchemaAndFlush();
+            if (totalBufferedRowCount >= 
writePlanFactory.inferBufferRowCount()) {
+                finalizePlanAndFlush();
             }
-        } else {
-            actualWriter.addElement(row);
+            return;
         }
+
+        actualWriter.addElement(row);
     }
 
     @Override
     public void writeBundle(BundleRecords bundle) throws IOException {
-        if (!schemaFinalized) {
+        if (!planFinalized) {
             final List<InternalRow> rows = new ArrayList<>();
-            bundle.forEach(row -> 
rows.add(InternalRowUtils.copyInternalRow(row, rowType)));
-            BundleRecords copiedBundle =
-                    new BundleRecords() {
-                        @Override
-                        @Nonnull
-                        public Iterator<InternalRow> iterator() {
-                            return rows.iterator();
-                        }
-
-                        @Override
-                        public long rowCount() {
-                            return rows.size();
-                        }
-                    };
-            bufferedBundles.add(copiedBundle);
+            for (InternalRow row : bundle) {
+                rows.add(InternalRowUtils.copyInternalRow(row, 
writePlanFactory.logicalRowType()));
+            }
+            bufferedBundles.add(new CopiedBundleRecords(rows));
             totalBufferedRowCount += bundle.rowCount();
-            if (totalBufferedRowCount >= maxBufferRow) {
-                finalizeSchemaAndFlush();
+            if (totalBufferedRowCount >= 
writePlanFactory.inferBufferRowCount()) {
+                finalizePlanAndFlush();
             }
-        } else {
-            ((BundleFormatWriter) actualWriter).writeBundle(bundle);
+            return;
         }
+
+        ((BundleFormatWriter) actualWriter).writeBundle(bundle);
     }
 
     @Override
     public boolean reachTargetSize(boolean suggestedCheck, long targetSize) 
throws IOException {
-        if (!schemaFinalized) {
+        if (!planFinalized) {
             return false;
         }
         return actualWriter.reachTargetSize(suggestedCheck, targetSize);
     }
 
+    @Nullable
+    @Override
+    public Object writerMetadata() {
+        return actualWriter == null ? null : actualWriter.writerMetadata();
+    }
+
     @Override
     public void close() throws IOException {
         try {
-            if (!schemaFinalized) {
-                finalizeSchemaAndFlush();
+            if (!planFinalized) {
+                finalizePlanAndFlush();
             }
         } finally {
             if (actualWriter != null) {
@@ -136,11 +122,12 @@ public class InferVariantShreddingWriter implements 
BundleFormatWriter {
         }
     }
 
-    private void finalizeSchemaAndFlush() throws IOException {
-        RowType inferredShreddingSchema = 
shreddingSchemaInfer.inferSchema(collectAllRows());
+    private void finalizePlanAndFlush() throws IOException {
+        ShreddingWritePlan writePlan = 
writePlanFactory.createWritePlan(collectAllRows());
         actualWriter =
-                writerFactory.createWithShreddingSchema(out, compression, 
inferredShreddingSchema);
-        schemaFinalized = true;
+                ShreddingWritePlanWriterFactory.createWriterWithPlan(
+                        writerFactory, out, compression, writePlan);
+        planFinalized = true;
 
         if (!bufferedBundles.isEmpty()) {
             BundleFormatWriter bundleWriter = (BundleFormatWriter) 
actualWriter;
@@ -157,16 +144,36 @@ public class InferVariantShreddingWriter implements 
BundleFormatWriter {
     }
 
     private List<InternalRow> collectAllRows() {
-        if (!bufferedBundles.isEmpty()) {
-            List<InternalRow> allRows = new ArrayList<>();
-            for (BundleRecords bundle : bufferedBundles) {
-                for (InternalRow row : bundle) {
-                    allRows.add(row);
-                }
-            }
-            return allRows;
-        } else {
+        if (bufferedBundles.isEmpty()) {
             return bufferedRows;
         }
+
+        List<InternalRow> allRows = new ArrayList<>();
+        for (BundleRecords bundle : bufferedBundles) {
+            for (InternalRow row : bundle) {
+                allRows.add(row);
+            }
+        }
+        return allRows;
+    }
+
+    private static class CopiedBundleRecords implements BundleRecords {
+
+        private final List<InternalRow> rows;
+
+        private CopiedBundleRecords(List<InternalRow> rows) {
+            this.rows = rows;
+        }
+
+        @Override
+        @Nonnull
+        public Iterator<InternalRow> iterator() {
+            return rows.iterator();
+        }
+
+        @Override
+        public long rowCount() {
+            return rows.size();
+        }
     }
 }
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
new file mode 100644
index 0000000000..504852819f
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingFormatWriter.java
@@ -0,0 +1,119 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.format.BundleFormatWriter;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.io.BundleRecords;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/** A writer wrapper that converts logical rows to the physical row layout 
before writing. */
+public class ShreddingFormatWriter implements BundleFormatWriter {
+
+    private final FormatWriter delegate;
+    private final SupportsShreddingWritePlan writerFactory;
+    private final ShreddingWritePlan writePlan;
+    private final String compression;
+
+    public ShreddingFormatWriter(
+            FormatWriter delegate,
+            SupportsShreddingWritePlan writerFactory,
+            ShreddingWritePlan writePlan,
+            String compression) {
+        this.delegate = delegate;
+        this.writerFactory = writerFactory;
+        this.writePlan = writePlan;
+        this.compression = compression;
+    }
+
+    @Override
+    public void addElement(InternalRow element) throws IOException {
+        delegate.addElement(writePlan.toPhysicalRow(element));
+    }
+
+    @Override
+    public void writeBundle(BundleRecords bundle) throws IOException {
+        if (delegate instanceof BundleFormatWriter) {
+            ((BundleFormatWriter) delegate).writeBundle(new 
PhysicalBundleRecords(bundle));
+            return;
+        }
+
+        for (InternalRow row : bundle) {
+            addElement(row);
+        }
+    }
+
+    @Override
+    public boolean reachTargetSize(boolean suggestedCheck, long targetSize) 
throws IOException {
+        return delegate.reachTargetSize(suggestedCheck, targetSize);
+    }
+
+    @Nullable
+    @Override
+    public Object writerMetadata() {
+        return delegate.writerMetadata();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            writerFactory.commitShreddingMetadata(delegate, writePlan, 
compression);
+        } finally {
+            delegate.close();
+        }
+    }
+
+    private class PhysicalBundleRecords implements BundleRecords {
+
+        private final BundleRecords delegateBundle;
+
+        private PhysicalBundleRecords(BundleRecords delegateBundle) {
+            this.delegateBundle = delegateBundle;
+        }
+
+        @Override
+        @Nonnull
+        public Iterator<InternalRow> iterator() {
+            final Iterator<InternalRow> iterator = delegateBundle.iterator();
+            return new Iterator<InternalRow>() {
+                @Override
+                public boolean hasNext() {
+                    return iterator.hasNext();
+                }
+
+                @Override
+                public InternalRow next() {
+                    return writePlan.toPhysicalRow(iterator.next());
+                }
+            };
+        }
+
+        @Override
+        public long rowCount() {
+            return delegateBundle.rowCount();
+        }
+    }
+}
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
new file mode 100644
index 0000000000..5adb313fe6
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.types.RowType;
+
+import java.util.List;
+
+/** Creates per-file shredding write plans. */
+public interface ShreddingWritePlanFactory {
+
+    RowType logicalRowType();
+
+    boolean shouldCreateWritePlan();
+
+    boolean shouldInferWritePlan();
+
+    int inferBufferRowCount();
+
+    ShreddingWritePlan createWritePlan(List<InternalRow> sampleRows);
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactories.java
 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactories.java
new file mode 100644
index 0000000000..1fba3ddedd
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactories.java
@@ -0,0 +1,49 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.shredding.MapSharedShreddingContext;
+import org.apache.paimon.data.shredding.MapSharedShreddingUtils;
+import org.apache.paimon.data.shredding.MapSharedShreddingWritePlanFactory;
+import org.apache.paimon.format.FormatWriterFactory;
+import org.apache.paimon.types.RowType;
+
+import java.util.List;
+
+/** Helpers for composing per-file shredding writer factories. */
+public class ShreddingWritePlanWriterFactories {
+
+    private ShreddingWritePlanWriterFactories() {}
+
+    public static FormatWriterFactory wrapMapSharedShredding(
+            FormatWriterFactory delegate, RowType rowType, CoreOptions 
options) {
+        List<String> shreddingFields =
+                MapSharedShreddingUtils.detectShreddingColumns(rowType, 
options);
+        if (shreddingFields.isEmpty()) {
+            return delegate;
+        }
+
+        MapSharedShreddingContext context =
+                new MapSharedShreddingContext(
+                        
MapSharedShreddingUtils.buildColumnToNumColumns(shreddingFields, options));
+        return new ShreddingWritePlanWriterFactory(
+                delegate, new MapSharedShreddingWritePlanFactory(rowType, 
context));
+    }
+}
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
new file mode 100644
index 0000000000..b3f6b2d75e
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/ShreddingWritePlanWriterFactory.java
@@ -0,0 +1,107 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.format.FormatWriterFactory;
+import org.apache.paimon.fs.PositionOutputStream;
+
+import java.io.IOException;
+import java.util.Collections;
+
+/** Decorates a format writer factory with optional per-file shredding write 
plans. */
+public class ShreddingWritePlanWriterFactory
+        implements FormatWriterFactory, SupportsShreddingWritePlan {
+
+    private final FormatWriterFactory delegate;
+    private final ShreddingWritePlanFactory writePlanFactory;
+
+    public ShreddingWritePlanWriterFactory(
+            FormatWriterFactory delegate, ShreddingWritePlanFactory 
writePlanFactory) {
+        this.delegate = delegate;
+        this.writePlanFactory = writePlanFactory;
+    }
+
+    @Override
+    public FormatWriter create(PositionOutputStream out, String compression) 
throws IOException {
+        if (!writePlanFactory.shouldCreateWritePlan()
+                || !(delegate instanceof SupportsShreddingWritePlan)) {
+            return delegate.create(out, compression);
+        }
+
+        SupportsShreddingWritePlan shreddingDelegate = 
(SupportsShreddingWritePlan) delegate;
+        if (writePlanFactory.shouldInferWritePlan()) {
+            return new InferShreddingWritePlanWriter(
+                    shreddingDelegate, writePlanFactory, out, compression);
+        }
+
+        return createWriterWithPlan(
+                shreddingDelegate,
+                out,
+                compression,
+                writePlanFactory.createWritePlan(Collections.emptyList()));
+    }
+
+    @Override
+    public FormatWriter createWithShreddingWritePlan(
+            PositionOutputStream out, String compression, ShreddingWritePlan 
writePlan)
+            throws IOException {
+        if (writePlanFactory.shouldCreateWritePlan()) {
+            throw new UnsupportedOperationException(
+                    "Composing multiple active shredding write plans is not 
supported.");
+        }
+
+        return shreddingDelegate().createWithShreddingWritePlan(out, 
compression, writePlan);
+    }
+
+    @Override
+    public void commitShreddingMetadata(
+            FormatWriter writer, ShreddingWritePlan writePlan, String 
compression)
+            throws IOException {
+        if (writePlanFactory.shouldCreateWritePlan()) {
+            throw new UnsupportedOperationException(
+                    "Composing multiple active shredding write plans is not 
supported.");
+        }
+
+        shreddingDelegate().commitShreddingMetadata(writer, writePlan, 
compression);
+    }
+
+    private SupportsShreddingWritePlan shreddingDelegate() {
+        if (!(delegate instanceof SupportsShreddingWritePlan)) {
+            throw new UnsupportedOperationException(
+                    "Delegate writer factory does not support shredding write 
plans: "
+                            + delegate.getClass().getName());
+        }
+        return (SupportsShreddingWritePlan) delegate;
+    }
+
+    static FormatWriter createWriterWithPlan(
+            SupportsShreddingWritePlan delegate,
+            PositionOutputStream out,
+            String compression,
+            ShreddingWritePlan writePlan)
+            throws IOException {
+        return new ShreddingFormatWriter(
+                delegate.createWithShreddingWritePlan(out, compression, 
writePlan),
+                delegate,
+                writePlan,
+                compression);
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/SupportsVariantInference.java
 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/SupportsShreddingWritePlan.java
similarity index 50%
rename from 
paimon-common/src/main/java/org/apache/paimon/format/variant/SupportsVariantInference.java
rename to 
paimon-common/src/main/java/org/apache/paimon/format/shredding/SupportsShreddingWritePlan.java
index 1fe404080e..f635eaae02 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/SupportsVariantInference.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/shredding/SupportsShreddingWritePlan.java
@@ -16,33 +16,22 @@
  * limitations under the License.
  */
 
-package org.apache.paimon.format.variant;
+package org.apache.paimon.format.shredding;
 
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
 import org.apache.paimon.format.FormatWriter;
 import org.apache.paimon.fs.PositionOutputStream;
-import org.apache.paimon.types.RowType;
 
 import java.io.IOException;
 
-/**
- * Interface for FormatWriterFactory implementations that support variant 
schema inference.
- *
- * <p>Writers implementing this interface can dynamically update their schema 
based on inferred
- * variant shredding schemas.
- */
-public interface SupportsVariantInference {
+/** Format writer factories that can create a writer for a shredded physical 
row layout. */
+public interface SupportsShreddingWritePlan {
 
-    /**
-     * Create the writer with the inferred shredding schema using the same 
output stream and
-     * compression settings.
-     *
-     * @param out The output stream to write to
-     * @param compression The compression codec
-     * @param inferredShreddingSchema The inferred shredding schema for 
variant fields
-     * @return A new FormatWriter configured with the inferred schema
-     * @throws IOException If the writer cannot be created
-     */
-    FormatWriter createWithShreddingSchema(
-            PositionOutputStream out, String compression, RowType 
inferredShreddingSchema)
+    FormatWriter createWithShreddingWritePlan(
+            PositionOutputStream out, String compression, ShreddingWritePlan 
writePlan)
             throws IOException;
+
+    default void commitShreddingMetadata(
+            FormatWriter writer, ShreddingWritePlan writePlan, String 
compression)
+            throws IOException {}
 }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceWriterFactory.java
 
b/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceWriterFactory.java
deleted file mode 100644
index cbbd3c5e19..0000000000
--- 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceWriterFactory.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.format.variant;
-
-import org.apache.paimon.format.FormatWriter;
-import org.apache.paimon.format.FormatWriterFactory;
-import org.apache.paimon.fs.PositionOutputStream;
-
-import java.io.IOException;
-
-/**
- * A decorator factory that adds variant schema inference capability to any 
{@link
- * FormatWriterFactory}.
- *
- * <p>This factory wraps an existing FormatWriterFactory and automatically 
enables variant schema
- * inference if the delegate factory supports it (implements {@link 
SupportsVariantInference}) and
- * the configuration enables inference.
- */
-public class VariantInferenceWriterFactory implements FormatWriterFactory {
-
-    private final FormatWriterFactory delegate;
-    private final VariantInferenceConfig config;
-
-    public VariantInferenceWriterFactory(
-            FormatWriterFactory delegate, VariantInferenceConfig config) {
-        this.delegate = delegate;
-        this.config = config;
-    }
-
-    @Override
-    public FormatWriter create(PositionOutputStream out, String compression) 
throws IOException {
-        if (!config.shouldEnableInference()) {
-            return delegate.create(out, compression);
-        }
-
-        if (!(delegate instanceof SupportsVariantInference)) {
-            return delegate.create(out, compression);
-        }
-
-        return new InferVariantShreddingWriter(
-                (SupportsVariantInference) delegate,
-                config.rowType(),
-                config.createInferrer(),
-                config.getMaxBufferRow(),
-                out,
-                compression);
-    }
-}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceConfig.java
 
b/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
similarity index 52%
rename from 
paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceConfig.java
rename to 
paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
index 991aa864e8..75e4c77e68 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantInferenceConfig.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/format/variant/VariantShreddingWritePlanFactory.java
@@ -19,26 +19,44 @@
 package org.apache.paimon.format.variant;
 
 import org.apache.paimon.CoreOptions;
+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.VariantShreddingWritePlan;
+import org.apache.paimon.format.shredding.ShreddingWritePlanFactory;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VariantType;
+import org.apache.paimon.utils.JsonSerdeUtil;
 
-/** Variant schema inference configuration. */
-public class VariantInferenceConfig {
+import java.util.List;
+
+/** Creates Variant shredding write plans from configured or inferred schemas. 
*/
+public class VariantShreddingWritePlanFactory implements 
ShreddingWritePlanFactory {
 
     private final RowType rowType;
     private final Options options;
 
-    public VariantInferenceConfig(RowType rowType, Options options) {
+    public VariantShreddingWritePlanFactory(RowType rowType, Options options) {
         this.rowType = rowType;
         this.options = options;
     }
 
-    /** Determines whether variant schema inference should be enabled. */
-    public boolean shouldEnableInference() {
-        if (options.contains(CoreOptions.VARIANT_SHREDDING_SCHEMA)) {
+    @Override
+    public RowType logicalRowType() {
+        return rowType;
+    }
+
+    @Override
+    public boolean shouldCreateWritePlan() {
+        return hasConfiguredShreddingSchema() || shouldInferWritePlan();
+    }
+
+    @Override
+    public boolean shouldInferWritePlan() {
+        if (hasConfiguredShreddingSchema()) {
             return false;
         }
 
@@ -49,17 +67,32 @@ public class VariantInferenceConfig {
         return containsVariantFields(rowType);
     }
 
-    private boolean containsVariantFields(RowType rowType) {
-        for (DataField field : rowType.getFields()) {
-            if (field.type() instanceof VariantType) {
-                return true;
-            }
+    @Override
+    public int inferBufferRowCount() {
+        return options.get(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW);
+    }
+
+    @Override
+    public ShreddingWritePlan createWritePlan(List<InternalRow> sampleRows) {
+        if (hasConfiguredShreddingSchema()) {
+            return VariantShreddingWritePlan.fromConfiguredSchema(
+                    rowType, configuredShreddingSchema());
         }
-        return false;
+
+        RowType physicalRowType = createInferrer().inferSchema(sampleRows);
+        return new VariantShreddingWritePlan(rowType, physicalRowType);
     }
 
-    /** Create a schema inferrer. */
-    public InferVariantShreddingSchema createInferrer() {
+    private boolean hasConfiguredShreddingSchema() {
+        return options.contains(CoreOptions.VARIANT_SHREDDING_SCHEMA);
+    }
+
+    private RowType configuredShreddingSchema() {
+        String shreddingSchema = 
options.get(CoreOptions.VARIANT_SHREDDING_SCHEMA);
+        return (RowType) JsonSerdeUtil.fromJson(shreddingSchema, 
DataType.class);
+    }
+
+    private InferVariantShreddingSchema createInferrer() {
         return new InferVariantShreddingSchema(
                 rowType,
                 options.get(CoreOptions.VARIANT_SHREDDING_MAX_SCHEMA_WIDTH),
@@ -67,12 +100,15 @@ public class VariantInferenceConfig {
                 
options.get(CoreOptions.VARIANT_SHREDDING_MIN_FIELD_CARDINALITY_RATIO));
     }
 
-    /** Get the maximum number of rows to buffer for inference. */
-    public int getMaxBufferRow() {
-        return options.get(CoreOptions.VARIANT_SHREDDING_MAX_INFER_BUFFER_ROW);
-    }
-
-    public RowType rowType() {
-        return rowType;
+    private boolean containsVariantFields(RowType rowType) {
+        for (DataField field : rowType.getFields()) {
+            if (field.type() instanceof VariantType) {
+                return true;
+            }
+            if (field.type() instanceof RowType && 
containsVariantFields((RowType) field.type())) {
+                return true;
+            }
+        }
+        return false;
     }
 }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingUtilsTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingUtilsTest.java
index 29314c6a00..e9d8d03f09 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingUtilsTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingUtilsTest.java
@@ -203,24 +203,27 @@ class MapSharedShreddingUtilsTest {
         MapSharedShreddingFieldMeta original =
                 new MapSharedShreddingFieldMeta(nameToId, fieldToColumns, 
overflowSet, 3, 2);
 
-        Map<String, String> metadata = new HashMap<>();
-        MapSharedShreddingUtils.serializeMetadata(original, "none", metadata);
-
-        
assertThat(MapSharedShreddingUtils.hasShreddingMetadata(metadata)).isTrue();
-        
assertThat(metadata.get(MapShreddingDefine.STORAGE_LAYOUT)).isEqualTo("shared-shredding");
-        
assertThat(metadata.get(MapSharedShreddingDefine.VERSION)).isEqualTo("1");
-        
assertThat(metadata.get(MapSharedShreddingDefine.NUM_COLUMNS)).isEqualTo("3");
-        
assertThat(metadata.get(MapSharedShreddingDefine.MAX_ROW_WIDTH)).isEqualTo("2");
-
         String expectedDict = "{\"age\":0,\"name\":1}";
-        
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT)).isEqualTo(expectedDict);
-        
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE))
-                .isEqualTo(String.valueOf(expectedDict.length()));
-        assertThat(metadata.get(MapSharedShreddingDefine.FIELD_COLUMNS))
-                .isEqualTo("{\"0\":[0],\"1\":[1,2]}");
-        
assertThat(metadata.get(MapSharedShreddingDefine.OVERFLOW_SET)).isEqualTo("[1,5]");
-        assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata, 
"none"))
-                .isEqualTo(original);
+        for (String compression : Arrays.<String>asList(null, "none", "NONE")) 
{
+            Map<String, String> metadata = new HashMap<>();
+            MapSharedShreddingUtils.serializeMetadata(original, compression, 
metadata);
+
+            
assertThat(MapSharedShreddingUtils.hasShreddingMetadata(metadata)).isTrue();
+            assertThat(metadata.get(MapShreddingDefine.STORAGE_LAYOUT))
+                    .isEqualTo("shared-shredding");
+            
assertThat(metadata.get(MapSharedShreddingDefine.VERSION)).isEqualTo("1");
+            
assertThat(metadata.get(MapSharedShreddingDefine.NUM_COLUMNS)).isEqualTo("3");
+            
assertThat(metadata.get(MapSharedShreddingDefine.MAX_ROW_WIDTH)).isEqualTo("2");
+
+            
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT)).isEqualTo(expectedDict);
+            
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE))
+                    .isEqualTo(String.valueOf(expectedDict.length()));
+            assertThat(metadata.get(MapSharedShreddingDefine.FIELD_COLUMNS))
+                    .isEqualTo("{\"0\":[0],\"1\":[1,2]}");
+            
assertThat(metadata.get(MapSharedShreddingDefine.OVERFLOW_SET)).isEqualTo("[1,5]");
+            assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata, 
compression))
+                    .isEqualTo(original);
+        }
     }
 
     @Test
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
new file mode 100644
index 0000000000..69010c2594
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.shredding;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericMap;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link MapSharedShreddingWritePlan}. */
+class MapSharedShreddingWritePlanTest {
+
+    @Test
+    void testConvertAndBuildFieldMetadata() {
+        RowType logicalType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(
+                                1, "tags", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.BIGINT())));
+        MapSharedShreddingContext context =
+                new MapSharedShreddingContext(Collections.singletonMap("tags", 
4));
+        MapSharedShreddingWritePlan writePlan =
+                new MapSharedShreddingWritePlan(logicalType, 
context.computeNextK(), context);
+
+        InternalRow physicalRow =
+                writePlan.toPhysicalRow(
+                        GenericRow.of(1, stringKeyMap("a", 10L, "b", 20L, "c", 
30L)));
+        InternalRow physicalTags = physicalRow.getRow(1, 6);
+
+        assertThat(writePlan.logicalRowType()).isEqualTo(logicalType);
+        assertThat(writePlan.physicalRowType().getFieldCount()).isEqualTo(2);
+        assertThat(physicalRow.getInt(0)).isEqualTo(1);
+        assertThat(physicalTags.getArray(0).toIntArray()).containsExactly(0, 
1, 2, -1);
+        assertThat(physicalTags.getLong(1)).isEqualTo(10L);
+        assertThat(physicalTags.getLong(2)).isEqualTo(20L);
+        assertThat(physicalTags.getLong(3)).isEqualTo(30L);
+        assertThat(physicalTags.isNullAt(4)).isTrue();
+        assertThat(physicalTags.isNullAt(5)).isTrue();
+
+        Map<String, Map<String, String>> fieldMetadata = 
writePlan.fieldMetadata("none");
+        assertThat(fieldMetadata).containsOnlyKeys("tags");
+        MapSharedShreddingFieldMeta fieldMeta =
+                
MapSharedShreddingUtils.deserializeMetadata(fieldMetadata.get("tags"), "none");
+        assertThat(fieldMeta.nameToId()).containsEntry("a", 
0).containsEntry("b", 1);
+        assertThat(fieldMeta.numColumns()).isEqualTo(4);
+        assertThat(fieldMeta.maxRowWidth()).isEqualTo(3);
+        assertThat(context.computeNextK()).containsEntry("tags", 3);
+    }
+
+    @Test
+    void testFactoryCreatesPlanFromContext() {
+        RowType logicalType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(
+                                0, "tags", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT())));
+        MapSharedShreddingContext context =
+                new MapSharedShreddingContext(Collections.singletonMap("tags", 
2));
+        MapSharedShreddingWritePlanFactory factory =
+                new MapSharedShreddingWritePlanFactory(logicalType, context);
+
+        assertThat(factory.shouldCreateWritePlan()).isTrue();
+        assertThat(factory.shouldInferWritePlan()).isFalse();
+        
assertThat(factory.createWritePlan(Collections.emptyList()).physicalRowType())
+                .isEqualTo(
+                        MapSharedShreddingUtils.logicalToPhysicalSchema(
+                                logicalType, Collections.singletonMap("tags", 
2)));
+    }
+
+    private static GenericMap stringKeyMap(Object... keyValues) {
+        Map<Object, Object> values = new LinkedHashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            values.put(BinaryString.fromString((String) keyValues[i]), 
keyValues[i + 1]);
+        }
+        return new GenericMap(values);
+    }
+}
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantShreddingWritePlanTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantShreddingWritePlanTest.java
new file mode 100644
index 0000000000..ae5a52a9b7
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantShreddingWritePlanTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.VariantType;
+
+import org.junit.jupiter.api.Test;
+
+import static 
org.apache.paimon.data.variant.PaimonShreddingUtils.assembleVariant;
+import static 
org.apache.paimon.data.variant.PaimonShreddingUtils.buildVariantSchema;
+import static 
org.apache.paimon.data.variant.PaimonShreddingUtils.variantShreddingSchema;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link VariantShreddingWritePlan}. */
+class VariantShreddingWritePlanTest {
+
+    @Test
+    void testConfiguredSchemaConvertsTopLevelVariant() {
+        RowType logicalType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(1, "v", DataTypes.VARIANT()));
+        RowType configuredSchema =
+                DataTypes.ROW(
+                        DataTypes.FIELD(
+                                1,
+                                "v",
+                                DataTypes.ROW(
+                                        DataTypes.FIELD(0, "age", 
DataTypes.BIGINT()),
+                                        DataTypes.FIELD(1, "name", 
DataTypes.STRING()))));
+
+        VariantShreddingWritePlan writePlan =
+                VariantShreddingWritePlan.fromConfiguredSchema(logicalType, 
configuredSchema);
+
+        
assertThat(writePlan.physicalRowType().getTypeAt(1)).isInstanceOf(RowType.class);
+        
assertThat(writePlan.physicalRowType().getTypeAt(1)).isNotInstanceOf(VariantType.class);
+
+        GenericVariant variant = 
GenericVariant.fromJson("{\"age\":30,\"name\":\"Alice\"}");
+        InternalRow physicalRow = writePlan.toPhysicalRow(GenericRow.of(1, 
variant));
+        RowType physicalVariantType = (RowType) 
writePlan.physicalRowType().getTypeAt(1);
+        InternalRow shreddedVariant = physicalRow.getRow(1, 
physicalVariantType.getFieldCount());
+
+        assertThat(physicalRow.getInt(0)).isEqualTo(1);
+        assertThat(
+                        assembleVariant(shreddedVariant, 
buildVariantSchema(physicalVariantType))
+                                .toJson())
+                .isEqualTo(variant.toJson());
+    }
+
+    @Test
+    void testNestedVariantConvertsRecursively() {
+        RowType nestedLogicalType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "payload", DataTypes.VARIANT()),
+                        DataTypes.FIELD(1, "name", DataTypes.STRING()));
+        RowType logicalType = DataTypes.ROW(DataTypes.FIELD(0, "nested", 
nestedLogicalType));
+
+        RowType payloadPhysicalType =
+                variantShreddingSchema(DataTypes.ROW(DataTypes.FIELD(0, 
"score", DataTypes.INT())));
+        RowType nestedPhysicalType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "payload", payloadPhysicalType),
+                        DataTypes.FIELD(1, "name", DataTypes.STRING()));
+        RowType physicalType = DataTypes.ROW(DataTypes.FIELD(0, "nested", 
nestedPhysicalType));
+
+        VariantShreddingWritePlan writePlan =
+                new VariantShreddingWritePlan(logicalType, physicalType);
+        GenericVariant variant = GenericVariant.fromJson("{\"score\":98}");
+        InternalRow physicalRow =
+                writePlan.toPhysicalRow(
+                        GenericRow.of(
+                                GenericRow.of(variant, 
BinaryString.fromString("attempt-1"))));
+
+        InternalRow nestedRow = physicalRow.getRow(0, 
nestedPhysicalType.getFieldCount());
+        InternalRow shreddedVariant = nestedRow.getRow(0, 
payloadPhysicalType.getFieldCount());
+
+        
assertThat(nestedRow.getString(1)).isEqualTo(BinaryString.fromString("attempt-1"));
+        assertThat(
+                        assembleVariant(shreddedVariant, 
buildVariantSchema(payloadPhysicalType))
+                                .toJson())
+                .isEqualTo(variant.toJson());
+    }
+}
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
new file mode 100644
index 0000000000..8345d2ae41
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/format/shredding/ShreddingFormatWriterTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link ShreddingFormatWriter}. */
+class ShreddingFormatWriterTest {
+
+    @Test
+    void testCloseDelegateWhenCommitMetadataFails() {
+        IOException failure = new IOException("metadata failed");
+        TestingFormatWriter delegate = new TestingFormatWriter();
+        ShreddingFormatWriter writer =
+                new ShreddingFormatWriter(
+                        delegate,
+                        new ThrowingMetadataFactory(failure),
+                        NoOpWritePlan.INSTANCE,
+                        "none");
+
+        assertThatThrownBy(writer::close).isSameAs(failure);
+        assertThat(delegate.closed).isTrue();
+    }
+
+    private static class TestingFormatWriter implements FormatWriter {
+
+        private boolean closed;
+
+        @Override
+        public void addElement(InternalRow element) {}
+
+        @Override
+        public boolean reachTargetSize(boolean suggestedCheck, long 
targetSize) {
+            return false;
+        }
+
+        @Override
+        public void close() {
+            closed = true;
+        }
+    }
+
+    private static class ThrowingMetadataFactory implements 
SupportsShreddingWritePlan {
+
+        private final IOException failure;
+
+        private ThrowingMetadataFactory(IOException failure) {
+            this.failure = failure;
+        }
+
+        @Override
+        public FormatWriter createWithShreddingWritePlan(
+                PositionOutputStream out, String compression, 
ShreddingWritePlan writePlan) {
+            return new TestingFormatWriter();
+        }
+
+        @Override
+        public void commitShreddingMetadata(
+                FormatWriter writer, ShreddingWritePlan writePlan, String 
compression)
+                throws IOException {
+            throw failure;
+        }
+    }
+
+    private enum NoOpWritePlan implements ShreddingWritePlan {
+        INSTANCE;
+
+        private final RowType rowType = new RowType(Collections.emptyList());
+
+        @Override
+        public RowType logicalRowType() {
+            return rowType;
+        }
+
+        @Override
+        public RowType physicalRowType() {
+            return rowType;
+        }
+
+        @Override
+        public InternalRow toPhysicalRow(InternalRow row) {
+            return row;
+        }
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcFileFormat.java 
b/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcFileFormat.java
index 01c2fa9517..5ac85779ab 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcFileFormat.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcFileFormat.java
@@ -154,7 +154,12 @@ public class OrcFileFormat extends FileFormat {
                         typeDescription, refinedType.getFields(), 
legacyTimestampLtzType);
 
         return new OrcWriterFactory(
-                vectorizer, orcProperties, writerConf, writeBatchSize, 
writeBatchMemory);
+                vectorizer,
+                orcProperties,
+                writerConf,
+                writeBatchSize,
+                writeBatchMemory,
+                legacyTimestampLtzType);
     }
 
     private Properties getOrcProperties(Options options, FormatContext 
formatContext) {
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcWriterFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcWriterFactory.java
index 8be5798e2f..948182825a 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcWriterFactory.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/orc/OrcWriterFactory.java
@@ -20,12 +20,18 @@ package org.apache.paimon.format.orc;
 
 import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.format.FormatMetadataUtils;
 import org.apache.paimon.format.FormatWriter;
 import org.apache.paimon.format.FormatWriterFactory;
+import org.apache.paimon.format.SupportsWriterMetadata;
 import org.apache.paimon.format.orc.writer.OrcBulkWriter;
+import org.apache.paimon.format.orc.writer.RowDataVectorizer;
 import org.apache.paimon.format.orc.writer.Vectorizer;
+import org.apache.paimon.format.shredding.SupportsShreddingWritePlan;
 import org.apache.paimon.fs.PositionOutputStream;
 import org.apache.paimon.options.MemorySize;
+import org.apache.paimon.types.RowType;
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FSDataOutputStream;
@@ -50,11 +56,12 @@ import static 
org.apache.paimon.utils.Preconditions.checkNotNull;
  * Vectorizer} implementation to convert the element into an {@link
  * org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch}.
  */
-public class OrcWriterFactory implements FormatWriterFactory {
+public class OrcWriterFactory implements FormatWriterFactory, 
SupportsShreddingWritePlan {
 
     private final Vectorizer<InternalRow> vectorizer;
     private final Properties writerProperties;
     private final Map<String, String> confMap;
+    private final boolean legacyTimestampLtzType;
 
     private OrcFile.WriterOptions writerOptions;
     private final int writeBatchSize;
@@ -68,26 +75,20 @@ public class OrcWriterFactory implements 
FormatWriterFactory {
      */
     @VisibleForTesting
     OrcWriterFactory(Vectorizer<InternalRow> vectorizer) {
-        this(vectorizer, new Properties(), new Configuration(false), 1024, 
MemorySize.ZERO);
+        this(vectorizer, new Properties(), new Configuration(false), 1024, 
MemorySize.ZERO, false);
     }
 
-    /**
-     * Creates a new OrcBulkWriterFactory using the provided Vectorizer, 
Hadoop Configuration, ORC
-     * writer properties.
-     *
-     * @param vectorizer The vectorizer implementation to convert input record 
to a
-     *     VectorizerRowBatch.
-     * @param writerProperties Properties that can be used in ORC 
WriterOptions.
-     */
     public OrcWriterFactory(
             Vectorizer<InternalRow> vectorizer,
             Properties writerProperties,
             Configuration configuration,
             int writeBatchSize,
-            MemorySize writeBatchMemory) {
+            MemorySize writeBatchMemory,
+            boolean legacyTimestampLtzType) {
         this.vectorizer = checkNotNull(vectorizer);
         this.writerProperties = checkNotNull(writerProperties);
         this.confMap = new HashMap<>();
+        this.legacyTimestampLtzType = legacyTimestampLtzType;
 
         // Todo: Replace the Map based approach with a better approach
         for (Map.Entry<String, String> entry : configuration) {
@@ -128,18 +129,59 @@ public class OrcWriterFactory implements 
FormatWriterFactory {
                 writeBatchMemory);
     }
 
+    @Override
+    public FormatWriter createWithShreddingWritePlan(
+            PositionOutputStream out, String compression, ShreddingWritePlan 
writePlan)
+            throws IOException {
+        RowType refinedType = (RowType) 
OrcFileFormat.refineDataType(writePlan.physicalRowType());
+        Vectorizer<InternalRow> physicalVectorizer =
+                new RowDataVectorizer(
+                        OrcTypeUtil.convertToOrcSchema(refinedType),
+                        refinedType.getFields(),
+                        legacyTimestampLtzType);
+        return new OrcWriterFactory(
+                        physicalVectorizer,
+                        writerProperties,
+                        configuration(),
+                        writeBatchSize,
+                        writeBatchMemory,
+                        legacyTimestampLtzType)
+                .create(out, compression);
+    }
+
+    @Override
+    public void commitShreddingMetadata(
+            FormatWriter writer, ShreddingWritePlan writePlan, String 
compression) {
+        Map<String, Map<String, String>> fieldMetadata = 
writePlan.fieldMetadata(compression);
+        if (fieldMetadata.isEmpty()) {
+            return;
+        }
+
+        Map<String, byte[]> metadata = new HashMap<>();
+        metadata.put(
+                FormatMetadataUtils.ARROW_SCHEMA_METADATA_KEY,
+                FormatMetadataUtils.buildArrowSchemaMetadata(
+                        writePlan.physicalRowType(),
+                        fieldMetadata,
+                        OrcTypeUtil.PAIMON_ORC_FIELD_ID_KEY));
+        ((SupportsWriterMetadata) writer).addMetadata(metadata);
+    }
+
     @VisibleForTesting
     protected OrcFile.WriterOptions getWriterOptions() {
         if (null == writerOptions) {
-            Configuration conf = new ThreadLocalClassLoaderConfiguration();
-            for (Map.Entry<String, String> entry : confMap.entrySet()) {
-                conf.set(entry.getKey(), entry.getValue());
-            }
-
-            writerOptions = OrcFile.writerOptions(writerProperties, conf);
+            writerOptions = OrcFile.writerOptions(writerProperties, 
configuration());
             writerOptions.setSchema(this.vectorizer.getSchema());
         }
 
         return writerOptions;
     }
+
+    private Configuration configuration() {
+        Configuration conf = new ThreadLocalClassLoaderConfiguration();
+        for (Map.Entry<String, String> entry : confMap.entrySet()) {
+            conf.set(entry.getKey(), entry.getValue());
+        }
+        return conf;
+    }
 }
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetFileFormat.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetFileFormat.java
index 66ab9565e7..7231be2de7 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetFileFormat.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetFileFormat.java
@@ -25,8 +25,8 @@ import org.apache.paimon.format.FormatReaderFactory;
 import org.apache.paimon.format.FormatWriterFactory;
 import org.apache.paimon.format.SimpleStatsExtractor;
 import org.apache.paimon.format.parquet.writer.RowDataParquetBuilder;
-import org.apache.paimon.format.variant.VariantInferenceConfig;
-import org.apache.paimon.format.variant.VariantInferenceWriterFactory;
+import org.apache.paimon.format.shredding.ShreddingWritePlanWriterFactory;
+import org.apache.paimon.format.variant.VariantShreddingWritePlanFactory;
 import org.apache.paimon.options.CatalogOptions;
 import org.apache.paimon.options.MemorySize;
 import org.apache.paimon.options.Options;
@@ -77,9 +77,8 @@ public class ParquetFileFormat extends FileFormat {
     public FormatWriterFactory createWriterFactory(RowType type) {
         ParquetWriterFactory baseFactory =
                 new ParquetWriterFactory(new RowDataParquetBuilder(type, 
options));
-        // Wrap with variant inference decorator
-        return new VariantInferenceWriterFactory(
-                baseFactory, new VariantInferenceConfig(type, 
formatContext.options()));
+        return new ShreddingWritePlanWriterFactory(
+                baseFactory, new VariantShreddingWritePlanFactory(type, 
formatContext.options()));
     }
 
     @Override
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetWriterFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetWriterFactory.java
index a26a0f3d1a..69e6d98678 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetWriterFactory.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetWriterFactory.java
@@ -19,18 +19,20 @@
 package org.apache.paimon.format.parquet;
 
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.ShreddingWritePlan;
+import org.apache.paimon.format.FormatMetadataUtils;
 import org.apache.paimon.format.FormatWriter;
 import org.apache.paimon.format.FormatWriterFactory;
 import org.apache.paimon.format.HadoopCompressionType;
+import org.apache.paimon.format.SupportsWriterMetadata;
 import org.apache.paimon.format.parquet.writer.MetadataParquetBuilder;
 import org.apache.paimon.format.parquet.writer.ParquetBuilder;
 import org.apache.paimon.format.parquet.writer.ParquetBulkWriter;
 import org.apache.paimon.format.parquet.writer.ParquetMetadataBulkWriter;
 import org.apache.paimon.format.parquet.writer.RowDataParquetBuilder;
 import org.apache.paimon.format.parquet.writer.StreamOutputFile;
-import org.apache.paimon.format.variant.SupportsVariantInference;
+import org.apache.paimon.format.shredding.SupportsShreddingWritePlan;
 import org.apache.paimon.fs.PositionOutputStream;
-import org.apache.paimon.types.RowType;
 
 import org.apache.parquet.hadoop.ParquetWriter;
 import org.apache.parquet.io.OutputFile;
@@ -40,7 +42,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 /** A factory that creates a Parquet {@link FormatWriter}. */
-public class ParquetWriterFactory implements FormatWriterFactory, 
SupportsVariantInference {
+public class ParquetWriterFactory implements FormatWriterFactory, 
SupportsShreddingWritePlan {
 
     /** The builder to construct the ParquetWriter. */
     private final ParquetBuilder<InternalRow> writerBuilder;
@@ -71,8 +73,8 @@ public class ParquetWriterFactory implements 
FormatWriterFactory, SupportsVarian
     }
 
     @Override
-    public FormatWriter createWithShreddingSchema(
-            PositionOutputStream stream, String compression, RowType 
inferredShreddingSchema)
+    public FormatWriter createWithShreddingWritePlan(
+            PositionOutputStream stream, String compression, 
ShreddingWritePlan writePlan)
             throws IOException {
         final OutputFile out = new StreamOutputFile(stream);
         if (HadoopCompressionType.NONE.value().equals(compression)) {
@@ -80,11 +82,28 @@ public class ParquetWriterFactory implements 
FormatWriterFactory, SupportsVarian
         }
 
         RowDataParquetBuilder newBuilder =
-                ((RowDataParquetBuilder) writerBuilder)
-                        .withShreddingSchemas(inferredShreddingSchema);
+                ((RowDataParquetBuilder) 
writerBuilder).withRowType(writePlan.physicalRowType());
         return createMetadataWriter(newBuilder, out, compression);
     }
 
+    @Override
+    public void commitShreddingMetadata(
+            FormatWriter writer, ShreddingWritePlan writePlan, String 
compression) {
+        Map<String, Map<String, String>> fieldMetadata = 
writePlan.fieldMetadata(compression);
+        if (fieldMetadata.isEmpty()) {
+            return;
+        }
+
+        Map<String, byte[]> metadata = new HashMap<>();
+        metadata.put(
+                FormatMetadataUtils.ARROW_SCHEMA_METADATA_KEY,
+                FormatMetadataUtils.buildArrowSchemaMetadata(
+                        writePlan.physicalRowType(),
+                        fieldMetadata,
+                        FormatMetadataUtils.PARQUET_FIELD_ID_KEY));
+        ((SupportsWriterMetadata) writer).addMetadata(metadata);
+    }
+
     private FormatWriter createMetadataWriter(
             MetadataParquetBuilder<InternalRow> builder, OutputFile out, 
String compression)
             throws IOException {
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantUtils.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantUtils.java
index 7fec5aee9f..2d3ed0e292 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantUtils.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantUtils.java
@@ -18,20 +18,13 @@
 
 package org.apache.paimon.format.parquet;
 
-import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.variant.PaimonShreddingUtils;
-import org.apache.paimon.options.Options;
 import org.apache.paimon.types.DataField;
-import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
-import org.apache.paimon.types.VariantType;
-import org.apache.paimon.utils.JsonSerdeUtil;
 
 import org.apache.parquet.schema.Type;
 
-import javax.annotation.Nullable;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -54,40 +47,4 @@ public class VariantUtils {
             return new RowType(dataFields);
         }
     }
-
-    /** For writer, extract shredding schemas from conf. */
-    @Nullable
-    public static RowType shreddingSchemasFromOptions(Options options) {
-        if (!options.contains(CoreOptions.VARIANT_SHREDDING_SCHEMA)) {
-            return null;
-        }
-
-        String shreddingSchema = 
options.get(CoreOptions.VARIANT_SHREDDING_SCHEMA);
-        RowType rowType = (RowType) JsonSerdeUtil.fromJson(shreddingSchema, 
DataType.class);
-        ArrayList<DataField> fields = new ArrayList<>();
-        for (DataField field : rowType.getFields()) {
-            
fields.add(field.newType(PaimonShreddingUtils.variantShreddingSchema(field.type())));
-        }
-        return new RowType(fields);
-    }
-
-    public static RowType replaceWithShreddingType(
-            RowType rowType, @Nullable RowType shreddingSchemas) {
-        if (shreddingSchemas == null) {
-            return rowType;
-        }
-
-        List<DataField> newFields = new ArrayList<>();
-        for (DataField field : rowType.getFields()) {
-            // todo: support nested variant.
-            if (field.type() instanceof VariantType
-                    && shreddingSchemas.containsField(field.name())) {
-                RowType shreddingSchema = (RowType) 
shreddingSchemas.getField(field.name()).type();
-                newFields.add(field.newType(shreddingSchema));
-            } else {
-                newFields.add(field);
-            }
-        }
-        return new RowType(rowType.isNullable(), newFields);
-    }
 }
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataBuilder.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataBuilder.java
index 60fd1097fe..8271d589c9 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataBuilder.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataBuilder.java
@@ -20,7 +20,6 @@ package org.apache.paimon.format.parquet.writer;
 
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.format.FormatMetadataUtils;
-import org.apache.paimon.format.parquet.VariantUtils;
 import org.apache.paimon.types.RowType;
 
 import org.apache.hadoop.conf.Configuration;
@@ -31,8 +30,6 @@ import org.apache.parquet.io.OutputFile;
 import org.apache.parquet.io.api.RecordConsumer;
 import org.apache.parquet.schema.MessageType;
 
-import javax.annotation.Nullable;
-
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Supplier;
@@ -44,14 +41,11 @@ public class ParquetRowDataBuilder
         extends ParquetWriter.Builder<InternalRow, ParquetRowDataBuilder> {
 
     private final RowType rowType;
-    @Nullable private final RowType shreddingSchemas;
     private Supplier<Map<String, byte[]>> metadataSupplier;
 
-    public ParquetRowDataBuilder(
-            OutputFile path, RowType rowType, @Nullable RowType 
shreddingSchemas) {
+    public ParquetRowDataBuilder(OutputFile path, RowType rowType) {
         super(path);
         this.rowType = rowType;
-        this.shreddingSchemas = shreddingSchemas;
         this.metadataSupplier = HashMap::new;
     }
 
@@ -68,23 +62,15 @@ public class ParquetRowDataBuilder
 
     @Override
     protected WriteSupport<InternalRow> getWriteSupport(Configuration conf) {
-        return new ParquetWriteSupport(conf);
+        return new ParquetWriteSupport();
     }
 
     private class ParquetWriteSupport extends WriteSupport<InternalRow> {
 
-        private final Configuration conf;
-        private final MessageType schema;
+        private final MessageType schema = 
convertToParquetMessageType(rowType);
 
         private ParquetRowDataWriter writer;
 
-        private ParquetWriteSupport(Configuration conf) {
-            this.conf = conf;
-            this.schema =
-                    convertToParquetMessageType(
-                            VariantUtils.replaceWithShreddingType(rowType, 
shreddingSchemas));
-        }
-
         @Override
         public WriteContext init(Configuration configuration) {
             return new WriteContext(schema, new HashMap<>());
@@ -92,9 +78,7 @@ public class ParquetRowDataBuilder
 
         @Override
         public void prepareForWrite(RecordConsumer recordConsumer) {
-            this.writer =
-                    new ParquetRowDataWriter(
-                            recordConsumer, rowType, schema, conf, 
shreddingSchemas);
+            this.writer = new ParquetRowDataWriter(recordConsumer, rowType, 
schema);
         }
 
         @Override
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataWriter.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataWriter.java
index 6a77b6cf20..a0e197e1da 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataWriter.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/ParquetRowDataWriter.java
@@ -25,10 +25,7 @@ import org.apache.paimon.data.InternalArray;
 import org.apache.paimon.data.InternalMap;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.data.Timestamp;
-import org.apache.paimon.data.variant.GenericVariant;
-import org.apache.paimon.data.variant.PaimonShreddingUtils;
 import org.apache.paimon.data.variant.Variant;
-import org.apache.paimon.data.variant.VariantSchema;
 import org.apache.paimon.format.parquet.ParquetSchemaConverter;
 import org.apache.paimon.types.ArrayType;
 import org.apache.paimon.types.DataType;
@@ -42,7 +39,6 @@ import org.apache.paimon.types.TimestampType;
 import org.apache.paimon.types.VariantType;
 import org.apache.paimon.types.VectorType;
 
-import org.apache.hadoop.conf.Configuration;
 import org.apache.parquet.io.api.Binary;
 import org.apache.parquet.io.api.RecordConsumer;
 import org.apache.parquet.schema.GroupType;
@@ -68,20 +64,11 @@ public class ParquetRowDataWriter {
     public static final long NANOS_PER_MILLISECOND = 
TimeUnit.MILLISECONDS.toNanos(1);
     public static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
 
-    private final Configuration conf;
     private final RowWriter rowWriter;
     private final RecordConsumer recordConsumer;
-    @Nullable private final RowType shreddingSchemas;
-
-    public ParquetRowDataWriter(
-            RecordConsumer recordConsumer,
-            RowType rowType,
-            GroupType schema,
-            Configuration conf,
-            @Nullable RowType shreddingSchemas) {
-        this.conf = conf;
+
+    public ParquetRowDataWriter(RecordConsumer recordConsumer, RowType 
rowType, GroupType schema) {
         this.recordConsumer = recordConsumer;
-        this.shreddingSchemas = shreddingSchemas;
         this.rowWriter = new RowWriter(rowType, schema);
     }
 
@@ -159,11 +146,7 @@ public class ParquetRowDataWriter {
             } else if (t instanceof RowType && type instanceof GroupType) {
                 return new RowWriter((RowType) t, groupType);
             } else if (t instanceof VariantType && type instanceof GroupType) {
-                RowType shreddingSchema =
-                        shreddingSchemas != null && 
shreddingSchemas.containsField(type.getName())
-                                ? (RowType) 
shreddingSchemas.getField(type.getName()).type()
-                                : null;
-                return new VariantWriter(groupType, shreddingSchema);
+                return new VariantWriter();
             } else {
                 throw new UnsupportedOperationException("Unsupported type: " + 
type);
             }
@@ -637,19 +620,6 @@ public class ParquetRowDataWriter {
 
     private class VariantWriter implements FieldWriter {
 
-        @Nullable private final VariantSchema variantSchema;
-        @Nullable private final RowWriter shreddedVariantWriter;
-
-        public VariantWriter(GroupType groupType, @Nullable RowType 
shreddingSchema) {
-            if (shreddingSchema != null) {
-                variantSchema = 
PaimonShreddingUtils.buildVariantSchema(shreddingSchema);
-                shreddedVariantWriter = new RowWriter(shreddingSchema, 
groupType);
-            } else {
-                variantSchema = null;
-                shreddedVariantWriter = null;
-            }
-        }
-
         @Override
         public void write(InternalRow row, int ordinal) {
             writeVariant(row.getVariant(ordinal));
@@ -661,15 +631,6 @@ public class ParquetRowDataWriter {
         }
 
         private void writeVariant(Variant variant) {
-            if (shreddedVariantWriter != null) {
-                recordConsumer.startGroup();
-                InternalRow shreddedVariant =
-                        PaimonShreddingUtils.castShredded((GenericVariant) 
variant, variantSchema);
-                shreddedVariantWriter.write(shreddedVariant);
-                recordConsumer.endGroup();
-                return;
-            }
-
             recordConsumer.startGroup();
             recordConsumer.startField(Variant.VALUE, 0);
             
recordConsumer.addBinary(Binary.fromReusedByteArray(variant.value()));
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
index 6239fc546b..10adbfe35b 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/writer/RowDataParquetBuilder.java
@@ -21,7 +21,6 @@ package org.apache.paimon.format.parquet.writer;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.format.HadoopCompressionType;
 import org.apache.paimon.format.parquet.ColumnConfigParser;
-import org.apache.paimon.format.parquet.VariantUtils;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.types.RowType;
 
@@ -32,8 +31,6 @@ import org.apache.parquet.hadoop.ParquetWriter;
 import org.apache.parquet.hadoop.metadata.CompressionCodecName;
 import org.apache.parquet.io.OutputFile;
 
-import javax.annotation.Nullable;
-
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
@@ -44,18 +41,20 @@ public class RowDataParquetBuilder implements 
MetadataParquetBuilder<InternalRow
 
     private final RowType rowType;
     private final Configuration conf;
-    @Nullable private RowType shreddingSchemas;
 
     public RowDataParquetBuilder(RowType rowType, Options options) {
         this.rowType = rowType;
         this.conf = new Configuration(false);
-        this.shreddingSchemas = 
VariantUtils.shreddingSchemasFromOptions(options);
         options.toMap().forEach(conf::set);
     }
 
-    public RowDataParquetBuilder withShreddingSchemas(RowType 
shreddingSchemas) {
-        this.shreddingSchemas = shreddingSchemas;
-        return this;
+    private RowDataParquetBuilder(RowType rowType, Configuration conf) {
+        this.rowType = rowType;
+        this.conf = conf;
+    }
+
+    public RowDataParquetBuilder withRowType(RowType rowType) {
+        return new RowDataParquetBuilder(rowType, conf);
     }
 
     @Override
@@ -69,7 +68,7 @@ public class RowDataParquetBuilder implements 
MetadataParquetBuilder<InternalRow
             OutputFile out, String compression, Supplier<Map<String, byte[]>> 
metadataSupplier)
             throws IOException {
         ParquetRowDataBuilder builder =
-                new ParquetRowDataBuilder(out, rowType, shreddingSchemas)
+                new ParquetRowDataBuilder(out, rowType)
                         .withMetadataSupplier(metadataSupplier)
                         .withConf(conf)
                         
.withCompressionCodec(getCompressionCodec(getCompression(compression)))
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/FileTypeNotMatchReadTypeTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/FileTypeNotMatchReadTypeTest.java
index c1d148116c..7afcbacc9d 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/FileTypeNotMatchReadTypeTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/FileTypeNotMatchReadTypeTest.java
@@ -90,9 +90,7 @@ public class FileTypeNotMatchReadTypeTest {
 
             ParquetRowDataBuilder parquetRowDataBuilder =
                     new ParquetRowDataBuilder(
-                            new LocalOutputFile(new 
File(fileWholePath).toPath()),
-                            rowTypeWrite,
-                            null);
+                            new LocalOutputFile(new 
File(fileWholePath).toPath()), rowTypeWrite);
 
             ParquetWriter<InternalRow> parquetWriter = 
parquetRowDataBuilder.build();
             Timestamp timestamp = Timestamp.now();
@@ -131,9 +129,7 @@ public class FileTypeNotMatchReadTypeTest {
 
             ParquetRowDataBuilder parquetRowDataBuilder =
                     new ParquetRowDataBuilder(
-                            new LocalOutputFile(new 
File(fileWholePath).toPath()),
-                            rowTypeWrite,
-                            null);
+                            new LocalOutputFile(new 
File(fileWholePath).toPath()), rowTypeWrite);
 
             ParquetWriter<InternalRow> parquetWriter = 
parquetRowDataBuilder.build();
             Decimal decimal =
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/ParquetRowDataBuilderForTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/ParquetRowDataBuilderForTest.java
index cd51332383..f4b0de67af 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/ParquetRowDataBuilderForTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/ParquetRowDataBuilderForTest.java
@@ -65,9 +65,7 @@ public class ParquetRowDataBuilderForTest
 
         @Override
         public void prepareForWrite(RecordConsumer recordConsumer) {
-            this.writer =
-                    new ParquetRowDataWriter(
-                            recordConsumer, rowType, schema, new 
Configuration(), null);
+            this.writer = new ParquetRowDataWriter(recordConsumer, rowType, 
schema);
         }
 
         @Override
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 4dc7c2b273..8c8cf5bd23 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
@@ -33,7 +33,7 @@ import org.apache.paimon.format.SupportsDirectWrite;
 import org.apache.paimon.format.parquet.ParquetFileFormat;
 import org.apache.paimon.format.parquet.ParquetUtil;
 import org.apache.paimon.format.parquet.VariantUtils;
-import org.apache.paimon.format.variant.InferVariantShreddingWriter;
+import org.apache.paimon.format.shredding.InferShreddingWritePlanWriter;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.PositionOutputStream;
@@ -63,7 +63,7 @@ import java.util.UUID;
 import static 
org.apache.paimon.data.variant.PaimonShreddingUtils.variantShreddingSchema;
 import static org.assertj.core.api.Assertions.assertThat;
 
-/** Test for {@link InferVariantShreddingWriter}. */
+/** Test for {@link InferShreddingWritePlanWriter}. */
 public class InferVariantShreddingWriteTest {
 
     @TempDir java.nio.file.Path tempPath;
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/shredding/ShreddingWritePlanFormatTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/shredding/ShreddingWritePlanFormatTest.java
new file mode 100644
index 0000000000..dfc8a485ab
--- /dev/null
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/shredding/ShreddingWritePlanFormatTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.format.shredding;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericMap;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.MapSharedShreddingFieldMeta;
+import org.apache.paimon.data.shredding.MapSharedShreddingUtils;
+import org.apache.paimon.data.shredding.MapShreddingDefine;
+import org.apache.paimon.format.FileFormat;
+import org.apache.paimon.format.FileFormatFactory;
+import org.apache.paimon.format.FormatMetadataUtils;
+import org.apache.paimon.format.FormatReaderContext;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.format.FormatWriterFactory;
+import org.apache.paimon.format.SupportsReaderFieldMetadata;
+import org.apache.paimon.format.orc.OrcFileFormat;
+import org.apache.paimon.format.orc.OrcTypeUtil;
+import org.apache.paimon.format.parquet.ParquetFileFormat;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.reader.FileRecordReader;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for format integration of {@link ShreddingWritePlanWriterFactory}. */
+class ShreddingWritePlanFormatTest {
+
+    @TempDir java.nio.file.Path tempDir;
+
+    @Test
+    void testParquetWritesMapSharedShreddingMetadataThroughVariantWrapper() 
throws Exception {
+        FileFormat format =
+                new ParquetFileFormat(
+                        new FileFormatFactory.FormatContext(new Options(), 
1024, 1024));
+
+        Map<String, Map<String, String>> fieldMetadata =
+                writeAndReadFieldMetadata(format, "parquet", "none");
+
+        assertMapSharedShreddingMetadata(fieldMetadata, "none");
+        assertThat(fieldMetadata.get("id"))
+                .containsEntry(FormatMetadataUtils.PARQUET_FIELD_ID_KEY, "0");
+        assertThat(fieldMetadata.get("tags"))
+                .containsEntry(FormatMetadataUtils.PARQUET_FIELD_ID_KEY, "1");
+    }
+
+    @Test
+    void testOrcWritesMapSharedShreddingMetadata() throws Exception {
+        FileFormat format =
+                new OrcFileFormat(new FileFormatFactory.FormatContext(new 
Options(), 1024, 1024));
+
+        Map<String, Map<String, String>> fieldMetadata =
+                writeAndReadFieldMetadata(format, "orc", "none");
+
+        assertMapSharedShreddingMetadata(fieldMetadata, "none");
+        
assertThat(fieldMetadata.get("id")).containsEntry(OrcTypeUtil.PAIMON_ORC_FIELD_ID_KEY,
 "0");
+        assertThat(fieldMetadata.get("tags"))
+                .containsEntry(OrcTypeUtil.PAIMON_ORC_FIELD_ID_KEY, "1");
+    }
+
+    private Map<String, Map<String, String>> writeAndReadFieldMetadata(
+            FileFormat format, String extension, String compression) throws 
IOException {
+        FileIO fileIO = LocalFileIO.create();
+        Path file = new Path(tempDir.toString(), UUID.randomUUID() + "." + 
extension);
+        RowType rowType = logicalRowType();
+        FormatWriterFactory writerFactory =
+                ShreddingWritePlanWriterFactories.wrapMapSharedShredding(
+                        format.createWriterFactory(rowType), rowType, 
mapSharedShreddingOptions());
+
+        PositionOutputStream out = fileIO.newOutputStream(file, false);
+        FormatWriter writer = writerFactory.create(out, compression);
+        writer.addElement(GenericRow.of(1, stringKeyMap("a", 10L, "b", 20L, 
"c", 30L)));
+        writer.close();
+        out.close();
+
+        RowType emptyRowType = new RowType(Collections.emptyList());
+        FormatReaderContext readerContext =
+                new FormatReaderContext(fileIO, file, 
fileIO.getFileSize(file));
+        try (FileRecordReader<InternalRow> reader =
+                format.createReaderFactory(emptyRowType, emptyRowType, 
Collections.emptyList())
+                        .createReader(readerContext)) {
+            return ((SupportsReaderFieldMetadata) reader).readFieldMetadata();
+        }
+    }
+
+    private static RowType logicalRowType() {
+        return DataTypes.ROW(
+                DataTypes.FIELD(0, "id", DataTypes.INT()),
+                DataTypes.FIELD(1, "tags", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.BIGINT())));
+    }
+
+    private static CoreOptions mapSharedShreddingOptions() {
+        Options options = new Options();
+        options.setString("fields.tags.map.storage-layout", 
"shared-shredding");
+        options.setString("fields.tags.map.shared-shredding.max-columns", "2");
+        return new CoreOptions(options);
+    }
+
+    private static void assertMapSharedShreddingMetadata(
+            Map<String, Map<String, String>> fieldMetadata, String 
compression) {
+        assertThat(fieldMetadata).containsKey("tags");
+        assertThat(fieldMetadata.get("tags"))
+                .containsEntry(
+                        MapShreddingDefine.STORAGE_LAYOUT,
+                        MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+
+        MapSharedShreddingFieldMeta fieldMeta =
+                
MapSharedShreddingUtils.deserializeMetadata(fieldMetadata.get("tags"), 
compression);
+        assertThat(fieldMeta.nameToId())
+                .containsEntry("a", 0)
+                .containsEntry("b", 1)
+                .containsEntry("c", 2);
+        assertThat(fieldMeta.fieldToColumns())
+                .containsEntry(0, Collections.singletonList(0))
+                .containsEntry(1, Collections.singletonList(1));
+        assertThat(fieldMeta.overflowFieldSet()).containsExactly(2);
+        assertThat(fieldMeta.numColumns()).isEqualTo(2);
+        assertThat(fieldMeta.maxRowWidth()).isEqualTo(3);
+    }
+
+    private static GenericMap stringKeyMap(Object... keyValues) {
+        Map<Object, Object> values = new LinkedHashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            values.put(BinaryString.fromString((String) keyValues[i]), 
keyValues[i + 1]);
+        }
+        return new GenericMap(values);
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/VariantTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/VariantTestBase.scala
index 34c48a5ba4..4cb3837071 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/VariantTestBase.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/VariantTestBase.scala
@@ -922,14 +922,13 @@ abstract class VariantTestBase extends 
PaimonSparkTestBase {
           | (2, struct('Bob', parse_json('{"score":88,"grade":"B"}')))
           | """.stripMargin)
 
-    checkAnswer(
-      sql("SELECT * FROM T ORDER BY id"),
-      sql("""
-            |SELECT 1, struct('Alice', parse_json('{"score":95,"grade":"A"}'))
-            |UNION ALL
-            |SELECT 2, struct('Bob', parse_json('{"score":88,"grade":"B"}'))
-            |""".stripMargin)
-    )
+    val result = sql("SELECT * FROM T ORDER BY id")
+    assert(result.collect().length == 2)
+    assert(result.schema.fieldNames.toSeq == Seq("id", "data"))
+    val dataSchema = result.schema("data").dataType.asInstanceOf[StructType]
+    assert(dataSchema.fieldNames.toSeq == Seq("name", "v"))
+    assert(isVariantType(dataSchema("v").dataType))
+
     checkAnswer(
       sql(
         "SELECT id, data.name, variant_get(data.v, '$.score', 'int'), 
variant_get(data.v, '$.grade', 'string') FROM T ORDER BY id"),
@@ -1010,10 +1009,12 @@ abstract class VariantTestBase extends 
PaimonSparkTestBase {
     checkAnswer(
       sql("SELECT * FROM T"),
       sql("""
-            |SELECT 1, struct(
+            |SELECT 1 AS id, named_struct(
+            |  'tags',
             |  array(parse_json('{"tag":"important","priority":1}'), 
parse_json('{"tag":"urgent","priority":2}')),
+            |  'attrs',
             |  map('color', parse_json('{"r":255,"g":0,"b":0}'), 'size', 
parse_json('{"width":100,"height":200}'))
-            |)
+            |) AS data
             |""".stripMargin)
     )
     checkAnswer(

Reply via email to