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 23fca3cd12 [core] Add shared-shredding map defs and utilities (#8314)
23fca3cd12 is described below

commit 23fca3cd125e93e5dad7141b55a3066ff008ebfd
Author: lxy <[email protected]>
AuthorDate: Mon Jun 22 17:28:45 2026 +0800

    [core] Add shared-shredding map defs and utilities (#8314)
    
    This PR adds the shared-shredding MAP schema utilities and option
    validation for [PIP-43: Columnar Storage Optimization for MAP Type in
    
Paimon](https://cwiki.apache.org/confluence/display/PAIMON/PIP-43%3A+Columnar+Storage+Optimization+for+MAP+Type+in+Paimon).
---
 .../main/java/org/apache/paimon/CoreOptions.java   |  58 ++++
 .../data/shredding/MapSharedShreddingDefine.java   |  44 +++
 .../shredding/MapSharedShreddingFieldMeta.java     |  88 ++++++
 .../data/shredding/MapSharedShreddingUtils.java    | 285 ++++++++++++++++++
 .../paimon/data/shredding/MapShreddingDefine.java  |  29 ++
 .../shredding/MapSharedShreddingUtilsTest.java     | 333 +++++++++++++++++++++
 .../org/apache/paimon/schema/SchemaValidation.java |  51 ++++
 .../java/org/apache/paimon/CoreOptionsTest.java    |  40 +++
 .../apache/paimon/schema/SchemaValidationTest.java | 103 +++++++
 9 files changed, 1031 insertions(+)

diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java 
b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index a884752434..bf85dd66dc 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -90,6 +90,11 @@ public class CoreOptions implements Serializable {
 
     public static final String MERGE_MAP_TS_FIELD = "ts-field";
 
+    public static final String MAP_STORAGE_LAYOUT = "map.storage-layout";
+
+    public static final String MAP_SHARED_SHREDDING_MAX_COLUMNS =
+            "map.shared-shredding.max-columns";
+
     public static final String FILE_INDEX = "file-index";
 
     public static final String COLUMNS = "columns";
@@ -4847,6 +4852,59 @@ public class CoreOptions implements Serializable {
                         .noDefaultValue());
     }
 
+    public MapStorageLayout mapStorageLayout(String fieldName) {
+        return options.get(
+                key(FIELDS_PREFIX + "." + fieldName + "." + MAP_STORAGE_LAYOUT)
+                        .enumType(MapStorageLayout.class)
+                        .defaultValue(MapStorageLayout.DEFAULT));
+    }
+
+    public int mapSharedShreddingMaxColumns(String fieldName) {
+        int maxColumns =
+                options.get(
+                        key(FIELDS_PREFIX
+                                        + "."
+                                        + fieldName
+                                        + "."
+                                        + MAP_SHARED_SHREDDING_MAX_COLUMNS)
+                                .intType()
+                                .defaultValue(256));
+        checkArgument(maxColumns > 0, "options %s must > 0", 
MAP_SHARED_SHREDDING_MAX_COLUMNS);
+        return maxColumns;
+    }
+
+    /** MAP storage layout. */
+    public enum MapStorageLayout implements DescribedEnum {
+        DEFAULT(
+                "default",
+                "Store MAP columns with the normal key-value array layout. 
This is the compatible "
+                        + "layout used when no field-level MAP layout option 
is configured."),
+        SHARED_SHREDDING(
+                "shared-shredding",
+                "Store MAP<STRING, T> columns as a physical row with reusable 
value columns, a "
+                        + "field mapping, and an overflow map. This layout is 
intended for maps "
+                        + "whose string keys repeat across rows and can 
benefit from more columnar "
+                        + "storage.");
+
+        private final String value;
+        private final String description;
+
+        MapStorageLayout(String value, String description) {
+            this.value = value;
+            this.description = description;
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        @Override
+        public InlineElement getDescription() {
+            return text(description);
+        }
+    }
+
     /**
      * Action to take when an UPDATE (e.g. via MERGE INTO) modifies columns 
that are covered by a
      * global index.
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingDefine.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingDefine.java
new file mode 100644
index 0000000000..6b5cce93d6
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingDefine.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/** Constants for the shared-shredding MAP storage layout. */
+public class MapSharedShreddingDefine {
+
+    public static final String VERSION = "paimon.map.shared-shredding.version";
+    public static final int CURRENT_VERSION = 1;
+    public static final String FIELD_DICT = 
"paimon.map.shared-shredding.field-dict";
+    public static final String FIELD_DICT_ORIGINAL_SIZE =
+            "paimon.map.shared-shredding.field-dict-original-size";
+    public static final String FIELD_COLUMNS = 
"paimon.map.shared-shredding.field-columns";
+    public static final String OVERFLOW_SET = 
"paimon.map.shared-shredding.overflow-set";
+    public static final String NUM_COLUMNS = 
"paimon.map.shared-shredding.num-columns";
+    public static final String MAX_ROW_WIDTH = 
"paimon.map.shared-shredding.max-row-width";
+
+    public static final String FIELD_MAPPING = "__field_mapping";
+    public static final String OVERFLOW = "__overflow";
+
+    public static final String DEFAULT_DICT_COMPRESSION = "zstd";
+
+    public static String physicalColumnName(int index) {
+        return "__col_" + index;
+    }
+
+    private MapSharedShreddingDefine() {}
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingFieldMeta.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingFieldMeta.java
new file mode 100644
index 0000000000..dea1928a34
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingFieldMeta.java
@@ -0,0 +1,88 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Parsed file-level metadata for one shared-shredding MAP column. */
+public class MapSharedShreddingFieldMeta {
+
+    private final Map<String, Integer> nameToId;
+    private final Map<Integer, List<Integer>> fieldToColumns;
+    private final Set<Integer> overflowFieldSet;
+    private final int numColumns;
+    private final int maxRowWidth;
+
+    public MapSharedShreddingFieldMeta(
+            Map<String, Integer> nameToId,
+            Map<Integer, List<Integer>> fieldToColumns,
+            Set<Integer> overflowFieldSet,
+            int numColumns,
+            int maxRowWidth) {
+        this.nameToId = nameToId;
+        this.fieldToColumns = fieldToColumns;
+        this.overflowFieldSet = overflowFieldSet;
+        this.numColumns = numColumns;
+        this.maxRowWidth = maxRowWidth;
+    }
+
+    public Map<String, Integer> nameToId() {
+        return nameToId;
+    }
+
+    public Map<Integer, List<Integer>> fieldToColumns() {
+        return fieldToColumns;
+    }
+
+    public Set<Integer> overflowFieldSet() {
+        return overflowFieldSet;
+    }
+
+    public int numColumns() {
+        return numColumns;
+    }
+
+    public int maxRowWidth() {
+        return maxRowWidth;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof MapSharedShreddingFieldMeta)) {
+            return false;
+        }
+        MapSharedShreddingFieldMeta that = (MapSharedShreddingFieldMeta) o;
+        return numColumns == that.numColumns
+                && maxRowWidth == that.maxRowWidth
+                && Objects.equals(nameToId, that.nameToId)
+                && Objects.equals(fieldToColumns, that.fieldToColumns)
+                && Objects.equals(overflowFieldSet, that.overflowFieldSet);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(nameToId, fieldToColumns, overflowFieldSet, 
numColumns, maxRowWidth);
+    }
+}
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
new file mode 100644
index 0000000000..6f0cd879c5
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingUtils.java
@@ -0,0 +1,285 @@
+/*
+ * 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.CoreOptions;
+import org.apache.paimon.CoreOptions.MapStorageLayout;
+import org.apache.paimon.compression.BlockCompressionFactory;
+import org.apache.paimon.compression.BlockCompressor;
+import org.apache.paimon.compression.BlockDecompressor;
+import org.apache.paimon.compression.CompressOptions;
+import org.apache.paimon.types.ArrayType;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypeRoot;
+import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.MapType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.JsonSerdeUtil;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.type.TypeReference;
+
+import javax.annotation.Nullable;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+/**
+ * Utility functions for the shared-shredding MAP storage layout.
+ *
+ * <p>Shared-shredding can be enabled for {@code MAP<STRING, T>} fields. It 
rewrites a logical MAP
+ * field into a physical ROW containing a key-to-column mapping array, a fixed 
number of value
+ * columns, and an overflow MAP for keys that cannot be placed in the fixed 
columns. This class also
+ * serializes and deserializes the per-field shredding metadata stored with 
the physical data,
+ * including optional compression for the field dictionary.
+ */
+public class MapSharedShreddingUtils {
+
+    private MapSharedShreddingUtils() {}
+
+    public static boolean isShreddingKeyMap(DataType dataType) {
+        if (!(dataType instanceof MapType)) {
+            return false;
+        }
+        MapType mapType = (MapType) dataType;
+        return mapType.getKeyType().getTypeRoot() == DataTypeRoot.VARCHAR;
+    }
+
+    public static List<String> detectShreddingColumns(RowType rowType, 
CoreOptions options) {
+        List<String> fieldNames = new ArrayList<>();
+        for (DataField field : rowType.getFields()) {
+            if (!isShreddingKeyMap(field.type())) {
+                continue;
+            }
+            if (options.mapStorageLayout(field.name()) == 
MapStorageLayout.SHARED_SHREDDING) {
+                fieldNames.add(field.name());
+            }
+        }
+        return fieldNames;
+    }
+
+    public static RowType logicalToPhysicalSchema(
+            RowType logicalSchema, Map<String, Integer> fieldToNumColumns) {
+        List<DataField> physicalFields = new ArrayList<>();
+        for (DataField field : logicalSchema.getFields()) {
+            Integer numColumns = fieldToNumColumns.get(field.name());
+            if (numColumns == null) {
+                physicalFields.add(field);
+                continue;
+            }
+
+            MapType mapType = (MapType) field.type();
+            DataType physicalType =
+                    buildPhysicalStructType(mapType.getValueType(), numColumns)
+                            .copy(field.type().isNullable());
+            physicalFields.add(field.newType(physicalType));
+        }
+        return new RowType(logicalSchema.isNullable(), physicalFields);
+    }
+
+    public static Map<String, Integer> buildColumnToNumColumns(
+            List<String> shreddingFieldNames, CoreOptions options) {
+        Map<String, Integer> fieldToNumColumns = new HashMap<>();
+        for (String fieldName : shreddingFieldNames) {
+            fieldToNumColumns.put(fieldName, 
options.mapSharedShreddingMaxColumns(fieldName));
+        }
+        return fieldToNumColumns;
+    }
+
+    public static void serializeMetadata(
+            MapSharedShreddingFieldMeta fieldMeta,
+            String compression,
+            Map<String, String> metadata) {
+        metadata.put(
+                MapShreddingDefine.STORAGE_LAYOUT,
+                MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+        metadata.put(
+                MapSharedShreddingDefine.VERSION,
+                String.valueOf(MapSharedShreddingDefine.CURRENT_VERSION));
+
+        String fieldDictJson = toJson(new TreeMap<>(fieldMeta.nameToId()));
+        metadata.put(
+                MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE,
+                
String.valueOf(fieldDictJson.getBytes(StandardCharsets.UTF_8).length));
+        metadata.put(
+                MapSharedShreddingDefine.FIELD_DICT,
+                bytesToString(
+                        
compress(fieldDictJson.getBytes(StandardCharsets.UTF_8), compression)));
+        metadata.put(
+                MapSharedShreddingDefine.FIELD_COLUMNS,
+                toJson(sortedFieldColumns(fieldMeta.fieldToColumns())));
+        metadata.put(
+                MapSharedShreddingDefine.OVERFLOW_SET,
+                toJson(new TreeSet<>(fieldMeta.overflowFieldSet())));
+        metadata.put(MapSharedShreddingDefine.NUM_COLUMNS, 
String.valueOf(fieldMeta.numColumns()));
+        metadata.put(
+                MapSharedShreddingDefine.MAX_ROW_WIDTH, 
String.valueOf(fieldMeta.maxRowWidth()));
+    }
+
+    public static MapSharedShreddingFieldMeta deserializeMetadata(
+            @Nullable Map<String, String> metadata, String compression) {
+        if (!hasShreddingMetadata(metadata)) {
+            throw new IllegalArgumentException(
+                    "metadata is null or storage layout is not 
shared-shredding");
+        }
+
+        int version = requiredInt(metadata, MapSharedShreddingDefine.VERSION);
+        if (version != MapSharedShreddingDefine.CURRENT_VERSION) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "unsupported shared-shredding metadata version: 
%s, expected: %s",
+                            version, 
MapSharedShreddingDefine.CURRENT_VERSION));
+        }
+
+        int originalLength =
+                requiredInt(metadata, 
MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE);
+        byte[] fieldDictBytes =
+                decompress(
+                        stringToBytes(requiredValue(metadata, 
MapSharedShreddingDefine.FIELD_DICT)),
+                        originalLength,
+                        compression);
+        Map<String, Integer> nameToId =
+                fromJson(
+                        new String(fieldDictBytes, StandardCharsets.UTF_8),
+                        new TypeReference<Map<String, Integer>>() {});
+        Map<Integer, List<Integer>> fieldToColumns =
+                parseFieldColumns(requiredValue(metadata, 
MapSharedShreddingDefine.FIELD_COLUMNS));
+        Set<Integer> overflowSet =
+                fromJson(
+                        requiredValue(metadata, 
MapSharedShreddingDefine.OVERFLOW_SET),
+                        new TypeReference<Set<Integer>>() {});
+
+        return new MapSharedShreddingFieldMeta(
+                nameToId,
+                fieldToColumns,
+                overflowSet,
+                requiredInt(metadata, MapSharedShreddingDefine.NUM_COLUMNS),
+                requiredInt(metadata, MapSharedShreddingDefine.MAX_ROW_WIDTH));
+    }
+
+    public static boolean hasShreddingMetadata(@Nullable Map<String, String> 
metadata) {
+        return metadata != null
+                && MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING.equals(
+                        metadata.get(MapShreddingDefine.STORAGE_LAYOUT));
+    }
+
+    private static RowType buildPhysicalStructType(DataType valueType, int 
numColumns) {
+        RowType.Builder builder = RowType.builder();
+        builder.field(MapSharedShreddingDefine.FIELD_MAPPING, new 
ArrayType(new IntType()));
+        for (int i = 0; i < numColumns; i++) {
+            builder.field(MapSharedShreddingDefine.physicalColumnName(i), 
valueType);
+        }
+        builder.field(MapSharedShreddingDefine.OVERFLOW, new MapType(new 
IntType(), valueType));
+        return builder.build();
+    }
+
+    private static Map<Integer, List<Integer>> sortedFieldColumns(
+            Map<Integer, List<Integer>> fieldToColumns) {
+        Map<Integer, List<Integer>> result = new TreeMap<>();
+        for (Map.Entry<Integer, List<Integer>> entry : 
fieldToColumns.entrySet()) {
+            List<Integer> columns = 
entry.getValue().stream().sorted().collect(Collectors.toList());
+            result.put(entry.getKey(), columns);
+        }
+        return result;
+    }
+
+    private static Map<Integer, List<Integer>> parseFieldColumns(String json) {
+        Map<String, List<Integer>> parsed =
+                fromJson(json, new TypeReference<Map<String, List<Integer>>>() 
{});
+        Map<Integer, List<Integer>> result = new TreeMap<>();
+        for (Map.Entry<String, List<Integer>> entry : parsed.entrySet()) {
+            result.put(Integer.parseInt(entry.getKey()), entry.getValue());
+        }
+        return result;
+    }
+
+    private static String requiredValue(Map<String, String> metadata, String 
key) {
+        String value = metadata.get(key);
+        if (value == null) {
+            throw new IllegalArgumentException("missing shredding metadata 
key: " + key);
+        }
+        return value;
+    }
+
+    private static int requiredInt(Map<String, String> metadata, String key) {
+        try {
+            return Integer.parseInt(requiredValue(metadata, key));
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    "malformed shredding metadata value for key: " + key, e);
+        }
+    }
+
+    private static byte[] compress(byte[] input, String compression) {
+        BlockCompressionFactory factory =
+                BlockCompressionFactory.create(new 
CompressOptions(compression, 1));
+        if (factory == null) {
+            return input;
+        }
+        BlockCompressor compressor = factory.getCompressor();
+        byte[] output = new 
byte[compressor.getMaxCompressedSize(input.length)];
+        int actualSize = compressor.compress(input, 0, input.length, output, 
0);
+        return Arrays.copyOf(output, actualSize);
+    }
+
+    private static byte[] decompress(byte[] input, int originalLength, String 
compression) {
+        BlockCompressionFactory factory =
+                BlockCompressionFactory.create(new 
CompressOptions(compression, 1));
+        if (factory == null) {
+            return input;
+        }
+        BlockDecompressor decompressor = factory.getDecompressor();
+        byte[] output = new byte[originalLength];
+        int actualSize = decompressor.decompress(input, 0, input.length, 
output, 0);
+        return Arrays.copyOf(output, actualSize);
+    }
+
+    private static String bytesToString(byte[] bytes) {
+        return new String(bytes, StandardCharsets.ISO_8859_1);
+    }
+
+    private static byte[] stringToBytes(String string) {
+        return string.getBytes(StandardCharsets.ISO_8859_1);
+    }
+
+    private static String toJson(Object object) {
+        try {
+            return 
JsonSerdeUtil.OBJECT_MAPPER_INSTANCE.writeValueAsString(object);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException("Failed to serialize shared-shredding 
metadata.", e);
+        }
+    }
+
+    private static <T> T fromJson(String json, TypeReference<T> typeReference) 
{
+        try {
+            return JsonSerdeUtil.OBJECT_MAPPER_INSTANCE.readValue(json, 
typeReference);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException("malformed shredding metadata", 
e);
+        }
+    }
+}
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapShreddingDefine.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapShreddingDefine.java
new file mode 100644
index 0000000000..ebc7d9a97e
--- /dev/null
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapShreddingDefine.java
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+/** Constants for MAP storage layout marker. */
+public class MapShreddingDefine {
+
+    public static final String STORAGE_LAYOUT = "paimon.map.storage-layout";
+
+    public static final String STORAGE_LAYOUT_SHARED_SHREDDING = 
"shared-shredding";
+
+    private MapShreddingDefine() {}
+}
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
new file mode 100644
index 0000000000..29314c6a00
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingUtilsTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.CoreOptions;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests for {@link MapSharedShreddingUtils}. */
+class MapSharedShreddingUtilsTest {
+
+    @Test
+    void testIsShreddingKeyMap() {
+        assertThat(
+                        MapSharedShreddingUtils.isShreddingKeyMap(
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT())))
+                .isTrue();
+        assertThat(
+                        MapSharedShreddingUtils.isShreddingKeyMap(
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.DOUBLE())))
+                .isTrue();
+        assertThat(
+                        MapSharedShreddingUtils.isShreddingKeyMap(
+                                DataTypes.MAP(
+                                        DataTypes.STRING(),
+                                        DataTypes.ROW(
+                                                DataTypes.FIELD(0, "x", 
DataTypes.INT()),
+                                                DataTypes.FIELD(1, "y", 
DataTypes.STRING())))))
+                .isTrue();
+        assertThat(
+                        MapSharedShreddingUtils.isShreddingKeyMap(
+                                DataTypes.MAP(DataTypes.INT(), 
DataTypes.STRING())))
+                .isFalse();
+        
assertThat(MapSharedShreddingUtils.isShreddingKeyMap(DataTypes.INT())).isFalse();
+        
assertThat(MapSharedShreddingUtils.isShreddingKeyMap(DataTypes.ARRAY(DataTypes.STRING())))
+                .isFalse();
+    }
+
+    @Test
+    void testDetectAndBuildColumnToNumColumns() {
+        RowType rowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(
+                                1, "tags", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.STRING())),
+                        DataTypes.FIELD(
+                                2,
+                                "metrics",
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.BIGINT())),
+                        DataTypes.FIELD(
+                                3, "codes", DataTypes.MAP(DataTypes.INT(), 
DataTypes.STRING())));
+
+        Options conf = new Options();
+        conf.setString("fields.tags.map.storage-layout", "shared-shredding");
+        conf.setString("fields.metrics.map.storage-layout", 
"shared-shredding");
+        conf.setString("fields.tags.map.shared-shredding.max-columns", "128");
+        conf.setString("fields.metrics.map.shared-shredding.max-columns", 
"64");
+        conf.setString("fields.codes.map.storage-layout", "shared-shredding");
+
+        CoreOptions options = new CoreOptions(conf);
+        assertThat(MapSharedShreddingUtils.detectShreddingColumns(rowType, 
options))
+                .containsExactly("tags", "metrics");
+        assertThat(
+                        MapSharedShreddingUtils.buildColumnToNumColumns(
+                                
MapSharedShreddingUtils.detectShreddingColumns(rowType, options),
+                                options))
+                .containsEntry("tags", 128)
+                .containsEntry("metrics", 64);
+
+        Options defaultConf = new Options();
+        CoreOptions defaultOptions = new CoreOptions(defaultConf);
+        assertThat(MapSharedShreddingUtils.detectShreddingColumns(rowType, 
defaultOptions))
+                .isEmpty();
+        assertThat(
+                        MapSharedShreddingUtils.buildColumnToNumColumns(
+                                Arrays.asList("tags"), defaultOptions))
+                .containsEntry("tags", 256);
+    }
+
+    @Test
+    void testLogicalToPhysicalSchema() {
+        RowType logical =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(
+                                1,
+                                "metrics",
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.DOUBLE().notNull())));
+
+        Map<String, Integer> fieldToNumColumns = new HashMap<>();
+        fieldToNumColumns.put("metrics", 2);
+
+        RowType physical =
+                MapSharedShreddingUtils.logicalToPhysicalSchema(logical, 
fieldToNumColumns);
+        DataField metrics = physical.getField("metrics");
+        assertThat(metrics.id()).isEqualTo(1);
+        assertThat(metrics.type()).isInstanceOf(RowType.class);
+        RowType metricsPhysicalType = (RowType) metrics.type();
+        assertThat(metricsPhysicalType.getFieldNames())
+                .containsExactly("__field_mapping", "__col_0", "__col_1", 
"__overflow");
+        assertThat(metricsPhysicalType.getFields())
+                .extracting(DataField::id)
+                .containsExactly(0, 1, 2, 3);
+        assertThat(metricsPhysicalType.getField("__col_0").type())
+                .isEqualTo(DataTypes.DOUBLE().notNull());
+        assertThat(metricsPhysicalType.getField("__overflow").type())
+                .isEqualTo(DataTypes.MAP(DataTypes.INT(), 
DataTypes.DOUBLE().notNull()));
+    }
+
+    @Test
+    void testLogicalToPhysicalSchemaNestedValueAndStableFieldIds() {
+        RowType nestedValue =
+                DataTypes.ROW(
+                        DataTypes.FIELD(2, "a", DataTypes.INT()),
+                        DataTypes.FIELD(3, "b", DataTypes.STRING()));
+        RowType original =
+                DataTypes.ROW(
+                        DataTypes.FIELD(1, "data", 
DataTypes.MAP(DataTypes.STRING(), nestedValue)));
+        RowType evolved =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "new_col", DataTypes.STRING()),
+                        DataTypes.FIELD(1, "data", 
DataTypes.MAP(DataTypes.STRING(), nestedValue)));
+
+        Map<String, Integer> fieldToNumColumns = new HashMap<>();
+        fieldToNumColumns.put("data", 2);
+
+        RowType physical =
+                MapSharedShreddingUtils.logicalToPhysicalSchema(original, 
fieldToNumColumns);
+        assertThat(physical.getField("data").id()).isEqualTo(1);
+        RowType dataPhysicalType = (RowType) physical.getField("data").type();
+        assertThat(dataPhysicalType.getFieldNames())
+                .containsExactly("__field_mapping", "__col_0", "__col_1", 
"__overflow");
+        assertThat(dataPhysicalType.getFields())
+                .extracting(DataField::id)
+                .containsExactly(0, 1, 2, 3);
+        
assertThat(dataPhysicalType.getField("__col_0").type()).isEqualTo(nestedValue);
+        assertThat(dataPhysicalType.getField("__overflow").type())
+                .isEqualTo(DataTypes.MAP(DataTypes.INT(), nestedValue));
+
+        RowType evolvedPhysical =
+                MapSharedShreddingUtils.logicalToPhysicalSchema(evolved, 
fieldToNumColumns);
+        assertThat(evolvedPhysical.getField("data").id()).isEqualTo(1);
+        assertThat(((RowType) 
evolvedPhysical.getField("data").type()).getFields())
+                .extracting(DataField::id)
+                .containsExactly(0, 1, 2, 3);
+    }
+
+    @Test
+    void testLogicalToPhysicalSchemaNoShreddingColumns() {
+        RowType logical =
+                DataTypes.ROW(
+                        DataTypes.FIELD(0, "id", DataTypes.INT()),
+                        DataTypes.FIELD(1, "name", DataTypes.STRING()));
+
+        assertThat(MapSharedShreddingUtils.logicalToPhysicalSchema(logical, 
new HashMap<>()))
+                .isEqualTo(logical);
+    }
+
+    @Test
+    void testMetadataRoundtrip() {
+        Map<String, Integer> nameToId = new TreeMap<>();
+        nameToId.put("age", 0);
+        nameToId.put("name", 1);
+
+        Map<Integer, List<Integer>> fieldToColumns = new TreeMap<>();
+        fieldToColumns.put(0, Arrays.asList(0));
+        fieldToColumns.put(1, Arrays.asList(1, 2));
+
+        HashSet<Integer> overflowSet = new HashSet<>();
+        overflowSet.add(1);
+        overflowSet.add(5);
+
+        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);
+    }
+
+    @Test
+    void testMetadataRoundtripCompression() {
+        Map<String, Integer> nameToId = new TreeMap<>();
+        nameToId.put("alpha", 0);
+        nameToId.put("beta", 1);
+        nameToId.put("gamma", 2);
+
+        Map<Integer, List<Integer>> fieldToColumns = new TreeMap<>();
+        fieldToColumns.put(0, Arrays.asList(0, 1, 2));
+        fieldToColumns.put(1, Arrays.asList(3));
+        fieldToColumns.put(2, Arrays.asList(4, 5));
+
+        HashSet<Integer> overflowSet = new HashSet<>();
+        overflowSet.add(2);
+        MapSharedShreddingFieldMeta original =
+                new MapSharedShreddingFieldMeta(nameToId, fieldToColumns, 
overflowSet, 6, 3);
+
+        for (String compression : Arrays.asList("none", "lz4", "zstd")) {
+            Map<String, String> metadata = new HashMap<>();
+            MapSharedShreddingUtils.serializeMetadata(original, compression, 
metadata);
+            assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata, 
compression))
+                    .isEqualTo(original);
+        }
+    }
+
+    @Test
+    void testMetadataRoundtripEmptyData() {
+        MapSharedShreddingFieldMeta original =
+                new MapSharedShreddingFieldMeta(
+                        new TreeMap<>(), new TreeMap<>(), new HashSet<>(), 0, 
0);
+
+        for (String compression : Arrays.asList("none", "lz4", "zstd")) {
+            Map<String, String> metadata = new HashMap<>();
+            MapSharedShreddingUtils.serializeMetadata(original, compression, 
metadata);
+            assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata, 
compression))
+                    .isEqualTo(original);
+        }
+    }
+
+    @Test
+    void testDeserializeMetadataErrors() {
+        assertThatThrownBy(() -> 
MapSharedShreddingUtils.deserializeMetadata(null, "none"))
+                .hasMessageContaining("metadata is null or storage layout is 
not shared-shredding");
+
+        Map<String, String> missingLayout = new HashMap<>();
+        missingLayout.put("some_key", "some_value");
+        assertThatThrownBy(() -> 
MapSharedShreddingUtils.deserializeMetadata(missingLayout, "none"))
+                .hasMessageContaining("metadata is null or storage layout is 
not shared-shredding");
+
+        Map<String, String> metadata = new HashMap<>();
+        metadata.put(MapShreddingDefine.STORAGE_LAYOUT, "default");
+        assertThatThrownBy(() -> 
MapSharedShreddingUtils.deserializeMetadata(metadata, "none"))
+                .hasMessageContaining("metadata is null or storage layout is 
not shared-shredding");
+
+        Map<String, String> missingVersion = new HashMap<>();
+        missingVersion.put(
+                MapShreddingDefine.STORAGE_LAYOUT,
+                MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+        assertThatThrownBy(
+                        () -> 
MapSharedShreddingUtils.deserializeMetadata(missingVersion, "none"))
+                .hasMessageContaining(
+                        "missing shredding metadata key: 
paimon.map.shared-shredding.version");
+
+        Map<String, String> wrongVersion = new HashMap<>();
+        wrongVersion.put(
+                MapShreddingDefine.STORAGE_LAYOUT,
+                MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+        wrongVersion.put(MapSharedShreddingDefine.VERSION, "999");
+        wrongVersion.put(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE, 
"2");
+        wrongVersion.put(MapSharedShreddingDefine.FIELD_DICT, "{}");
+        assertThatThrownBy(() -> 
MapSharedShreddingUtils.deserializeMetadata(wrongVersion, "none"))
+                .hasMessageContaining("unsupported shared-shredding metadata 
version: 999");
+
+        Map<String, String> missingFieldDict = new HashMap<>();
+        missingFieldDict.put(
+                MapShreddingDefine.STORAGE_LAYOUT,
+                MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+        missingFieldDict.put(MapSharedShreddingDefine.VERSION, "1");
+        
missingFieldDict.put(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE, "2");
+        assertThatThrownBy(
+                        () -> 
MapSharedShreddingUtils.deserializeMetadata(missingFieldDict, "none"))
+                .hasMessageContaining(
+                        "missing shredding metadata key: 
paimon.map.shared-shredding.field-dict");
+    }
+
+    @Test
+    void testHasShreddingMetadata() {
+        
assertThat(MapSharedShreddingUtils.hasShreddingMetadata(null)).isFalse();
+
+        Map<String, String> metadata = new HashMap<>();
+        metadata.put(
+                MapShreddingDefine.STORAGE_LAYOUT,
+                MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
+        
assertThat(MapSharedShreddingUtils.hasShreddingMetadata(metadata)).isTrue();
+
+        metadata.put(MapShreddingDefine.STORAGE_LAYOUT, "default");
+        
assertThat(MapSharedShreddingUtils.hasShreddingMetadata(metadata)).isFalse();
+
+        assertThat(MapSharedShreddingUtils.hasShreddingMetadata(new 
HashMap<>())).isFalse();
+    }
+
+    @Test
+    void testPhysicalColumnName() {
+        
assertThat(MapSharedShreddingDefine.physicalColumnName(0)).isEqualTo("__col_0");
+        
assertThat(MapSharedShreddingDefine.physicalColumnName(1)).isEqualTo("__col_1");
+        
assertThat(MapSharedShreddingDefine.physicalColumnName(99)).isEqualTo("__col_99");
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java 
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
index 7733b6a080..fedfecaafb 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
@@ -20,8 +20,10 @@ package org.apache.paimon.schema;
 
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.CoreOptions.ChangelogProducer;
+import org.apache.paimon.CoreOptions.MapStorageLayout;
 import org.apache.paimon.CoreOptions.MergeEngine;
 import org.apache.paimon.TableType;
+import org.apache.paimon.data.shredding.MapSharedShreddingUtils;
 import org.apache.paimon.factories.FactoryUtil;
 import org.apache.paimon.fileindex.FileIndexOptions;
 import org.apache.paimon.fileindex.FileIndexerFactory;
@@ -69,6 +71,7 @@ import static 
org.apache.paimon.CoreOptions.FULL_COMPACTION_DELTA_COMMITS;
 import static org.apache.paimon.CoreOptions.INCREMENTAL_BETWEEN;
 import static org.apache.paimon.CoreOptions.INCREMENTAL_BETWEEN_TIMESTAMP;
 import static org.apache.paimon.CoreOptions.INCREMENTAL_TO_AUTO_TAG;
+import static org.apache.paimon.CoreOptions.MAP_STORAGE_LAYOUT;
 import static org.apache.paimon.CoreOptions.PRIMARY_KEY;
 import static org.apache.paimon.CoreOptions.SCAN_FILE_CREATION_TIME_MILLIS;
 import static org.apache.paimon.CoreOptions.SCAN_MODE;
@@ -355,6 +358,8 @@ public class SchemaValidation {
         validatePkClusteringOverride(options);
 
         validateManifestSort(schema, options);
+
+        validateMapStorageLayout(schema, options);
     }
 
     public static void validateFallbackBranch(SchemaManager schemaManager, 
TableSchema schema) {
@@ -604,6 +609,52 @@ public class SchemaValidation {
         createMergeFunctionFactory(schema);
     }
 
+    private static void validateMapStorageLayout(TableSchema schema, 
CoreOptions options) {
+        String layoutSuffix = "." + MAP_STORAGE_LAYOUT;
+        Map<String, DataField> fieldMap = new HashMap<>();
+        for (DataField field : schema.fields()) {
+            fieldMap.put(field.name(), field);
+        }
+
+        for (String key : options.toMap().keySet()) {
+            if (!key.startsWith(FIELDS_PREFIX + ".") || 
!key.endsWith(layoutSuffix)) {
+                continue;
+            }
+
+            String fieldName =
+                    key.substring(
+                            (FIELDS_PREFIX + ".").length(), key.length() - 
layoutSuffix.length());
+            DataField field = fieldMap.get(fieldName);
+            if (field == null) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Column '%s' is configured with 
map.storage-layout but does not exist in table schema.",
+                                fieldName));
+            }
+
+            DataType fieldType = field.type();
+            if (!(fieldType instanceof MapType)) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Column '%s' is configured with 
map.storage-layout but its type is not MAP.",
+                                fieldName));
+            }
+
+            MapStorageLayout layout = options.mapStorageLayout(fieldName);
+            if (layout != MapStorageLayout.SHARED_SHREDDING) {
+                continue;
+            }
+
+            if (!MapSharedShreddingUtils.isShreddingKeyMap(fieldType)) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Column '%s' is configured with 
map.storage-layout=shared-shredding but its type is not MAP<STRING, T>.",
+                                fieldName));
+            }
+            options.mapSharedShreddingMaxColumns(fieldName);
+        }
+    }
+
     private static void validateFileIndex(TableSchema schema) {
         CoreOptions options = new CoreOptions(schema.options());
         FileIndexOptions fileIndexOptions = options.indexColumnsOptions();
diff --git a/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java 
b/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
index 359b9f25e8..3399990a8a 100644
--- a/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/CoreOptionsTest.java
@@ -23,6 +23,7 @@ import org.apache.paimon.options.Options;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Tests for {@link org.apache.paimon.CoreOptions}. */
 public class CoreOptionsTest {
@@ -117,4 +118,43 @@ public class CoreOptionsTest {
         options = new CoreOptions(conf);
         assertThat(options.blobSplitByFileSize()).isTrue();
     }
+
+    @Test
+    public void testMapStorageLayout() {
+        Options conf = new Options();
+        CoreOptions options = new CoreOptions(conf);
+        assertThat(options.mapStorageLayout("metrics"))
+                .isEqualTo(CoreOptions.MapStorageLayout.DEFAULT);
+        
assertThat(options.mapSharedShreddingMaxColumns("metrics")).isEqualTo(256);
+
+        conf.setString("fields.metrics.map.storage-layout", 
"shared-shredding");
+        conf.setString("fields.metrics.map.shared-shredding.max-columns", 
"32");
+        options = new CoreOptions(conf);
+        assertThat(options.mapStorageLayout("metrics"))
+                .isEqualTo(CoreOptions.MapStorageLayout.SHARED_SHREDDING);
+        
assertThat(options.mapSharedShreddingMaxColumns("metrics")).isEqualTo(32);
+
+        conf = new Options();
+        conf.setString("fields.metrics.map.storage-layout", 
"Shared-Shredding");
+        options = new CoreOptions(conf);
+        assertThat(options.mapStorageLayout("metrics"))
+                .isEqualTo(CoreOptions.MapStorageLayout.SHARED_SHREDDING);
+
+        conf = new Options();
+        conf.setString("fields.metrics.map.storage-layout", "invalid");
+        final CoreOptions invalidLayoutOptions = new CoreOptions(conf);
+        assertThatThrownBy(() -> 
invalidLayoutOptions.mapStorageLayout("metrics"))
+                .hasMessageContaining("invalid");
+
+        conf = new Options();
+        conf.setString("fields.metrics.map.shared-shredding.max-columns", "0");
+        final CoreOptions zeroMaxColumnsOptions = new CoreOptions(conf);
+        assertThatThrownBy(() -> 
zeroMaxColumnsOptions.mapSharedShreddingMaxColumns("metrics"))
+                .hasMessageContaining("options 
map.shared-shredding.max-columns must > 0");
+
+        conf.setString("fields.metrics.map.shared-shredding.max-columns", 
"-1");
+        final CoreOptions negativeMaxColumnsOptions = new CoreOptions(conf);
+        assertThatThrownBy(() -> 
negativeMaxColumnsOptions.mapSharedShreddingMaxColumns("metrics"))
+                .hasMessageContaining("options 
map.shared-shredding.max-columns must > 0");
+    }
 }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java 
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
index a306adc670..ee3e25846a 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaValidationTest.java
@@ -210,6 +210,109 @@ class SchemaValidationTest {
                 .hasMessageContaining("fields.data.num.aggregate-function");
     }
 
+    @Test
+    public void testMapStorageLayout() {
+        List<DataField> fields =
+                Arrays.asList(
+                        new DataField(0, "id", DataTypes.INT()),
+                        new DataField(
+                                1, "metrics", 
DataTypes.MAP(DataTypes.STRING(), DataTypes.INT())),
+                        new DataField(
+                                2, "codes", DataTypes.MAP(DataTypes.INT(), 
DataTypes.STRING())));
+
+        Map<String, String> options = new HashMap<>();
+        options.put(BUCKET.key(), "-1");
+        options.put("fields.metrics.map.storage-layout", "shared-shredding");
+        assertThatNoException()
+                .isThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")));
+
+        options.put("fields.metrics.map.storage-layout", "default");
+        assertThatNoException()
+                .isThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")));
+
+        options.put("fields.nonexist.map.storage-layout", "shared-shredding");
+        assertThatThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")))
+                .hasMessageContaining("Field nonexist can not be found in 
table schema.");
+
+        options.remove("fields.nonexist.map.storage-layout");
+        options.put("fields.id.map.storage-layout", "default");
+        assertThatThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")))
+                .hasMessageContaining(
+                        "Column 'id' is configured with map.storage-layout but 
its type is not MAP.");
+
+        options.remove("fields.id.map.storage-layout");
+        options.put("fields.codes.map.storage-layout", "shared-shredding");
+        assertThatThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")))
+                .hasMessageContaining(
+                        "Column 'codes' is configured with 
map.storage-layout=shared-shredding but its type is not MAP<STRING, T>.");
+
+        options.remove("fields.codes.map.storage-layout");
+        options.put("fields.metrics.map.storage-layout", "shared-shredding");
+        options.put("fields.metrics.map.shared-shredding.max-columns", "0");
+        assertThatThrownBy(
+                        () ->
+                                validateTableSchema(
+                                        new TableSchema(
+                                                1,
+                                                fields,
+                                                10,
+                                                emptyList(),
+                                                emptyList(),
+                                                options,
+                                                "")))
+                .hasMessageContaining("options 
map.shared-shredding.max-columns must > 0");
+    }
+
     @Test
     public void testChainTableAllowsNonDeduplicateMergeEngine() {
         Map<String, String> options = new HashMap<>();

Reply via email to