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 428c18fa83 [core] Validate unsupported MAP shared-shredding
configurations (#8487)
428c18fa83 is described below
commit 428c18fa836c7d5f5e29e54dc68a41cc19195568
Author: lxy <[email protected]>
AuthorDate: Tue Jul 7 15:20:18 2026 +0800
[core] Validate unsupported MAP shared-shredding configurations (#8487)
This is a subtask of [MAP shared-shredding
support](https://cwiki.apache.org/confluence/display/PAIMON/PIP-43%3A+Columnar+Storage+Optimization+for+MAP+Type+in+Paimon).
This PR splits out the validation and common compatibility work for MAP
shared-shredding so it can be reviewed independently before the
read/write path is wired.
It adds validation for unsupported MAP shared-shredding configurations,
including table modes, file formats, compression settings, nested file
index usage, and unsupported value types such as Variant, BLOB, and
MULTISET.
It also includes small common-layer fixes needed by later
shared-shredding materialization, such as field metadata compression
persistence and `RowToColumnConverter` handling for
CHAR/VARCHAR/BINARY/VARBINARY.
This PR does not yet enable reading or writing MAP shared-shredding
tables. If configured, the option is validated but the functional append
read/write path will be added in the next PR, together with append-table
E2E tests.
---
paimon-common/pom.xml | 1 +
.../paimon/data/columnar/RowToColumnConverter.java | 256 +++++++++++++-
.../data/shredding/MapSharedShreddingDefine.java | 2 +
.../data/shredding/MapSharedShreddingUtils.java | 45 ++-
.../data/columnar/RowToColumnConverterTest.java | 388 +++++++++++++++++++++
.../shredding/MapSharedShreddingUtilsTest.java | 61 +++-
.../shredding/MapSharedShreddingWritePlanTest.java | 4 +-
.../org/apache/paimon/schema/SchemaValidation.java | 126 ++++++-
.../apache/paimon/schema/SchemaValidationTest.java | 241 +++++++++++++
9 files changed, 1095 insertions(+), 29 deletions(-)
diff --git a/paimon-common/pom.xml b/paimon-common/pom.xml
index 961559c9b8..4ba3aac399 100644
--- a/paimon-common/pom.xml
+++ b/paimon-common/pom.xml
@@ -300,6 +300,7 @@ under the License.
<artifactSet>
<includes combine.children="append">
<include>org.antlr:antlr4-runtime</include>
+ <include>io.airlift:aircompressor</include>
<include>org.codehaus.janino:*</include>
<include>it.unimi.dsi:fastutil</include>
<include>org.roaringbitmap:RoaringBitmap</include>
diff --git
a/paimon-common/src/main/java/org/apache/paimon/data/columnar/RowToColumnConverter.java
b/paimon-common/src/main/java/org/apache/paimon/data/columnar/RowToColumnConverter.java
index f3fdba7b09..e35c7d77b1 100644
---
a/paimon-common/src/main/java/org/apache/paimon/data/columnar/RowToColumnConverter.java
+++
b/paimon-common/src/main/java/org/apache/paimon/data/columnar/RowToColumnConverter.java
@@ -18,6 +18,8 @@
package org.apache.paimon.data.columnar;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Blob;
import org.apache.paimon.data.DataGetters;
import org.apache.paimon.data.Decimal;
import org.apache.paimon.data.InternalArray;
@@ -39,6 +41,7 @@ import
org.apache.paimon.data.columnar.writable.WritableIntVector;
import org.apache.paimon.data.columnar.writable.WritableLongVector;
import org.apache.paimon.data.columnar.writable.WritableShortVector;
import org.apache.paimon.data.columnar.writable.WritableTimestampVector;
+import org.apache.paimon.data.variant.GenericVariant;
import org.apache.paimon.data.variant.Variant;
import org.apache.paimon.types.ArrayType;
import org.apache.paimon.types.BigIntType;
@@ -88,6 +91,237 @@ public class RowToColumnConverter {
}
}
+ /** Create a reusable element converter for materializing column vectors.
*/
+ public static ElementConverter createElementConverter(DataType type) {
+ return new ElementConverter(TypeConverter.getConverterForType(type));
+ }
+
+ /** Converts one element into a writable column vector. */
+ public static class ElementConverter implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final TypeConverter converter;
+
+ private ElementConverter(TypeConverter converter) {
+ this.converter = converter;
+ }
+
+ public void append(DataGetters getters, int pos, WritableColumnVector
vector) {
+ converter.append(getters, pos, vector);
+ }
+
+ public void append(ColumnVector source, int rowId,
WritableColumnVector vector) {
+ converter.append(new ColumnVectorDataGetters(source, rowId), 0,
vector);
+ }
+
+ public void append(Object value, WritableColumnVector vector) {
+ converter.append(new ObjectDataGetters(value), 0, vector);
+ }
+ }
+
+ private static class ColumnVectorDataGetters implements DataGetters {
+
+ private final ColumnVector vector;
+ private final int rowId;
+
+ private ColumnVectorDataGetters(ColumnVector vector, int rowId) {
+ this.vector = vector;
+ this.rowId = rowId;
+ }
+
+ @Override
+ public boolean isNullAt(int pos) {
+ return vector.isNullAt(rowId);
+ }
+
+ @Override
+ public boolean getBoolean(int pos) {
+ return ((BooleanColumnVector) vector).getBoolean(rowId);
+ }
+
+ @Override
+ public byte getByte(int pos) {
+ return ((ByteColumnVector) vector).getByte(rowId);
+ }
+
+ @Override
+ public short getShort(int pos) {
+ return ((ShortColumnVector) vector).getShort(rowId);
+ }
+
+ @Override
+ public int getInt(int pos) {
+ return ((IntColumnVector) vector).getInt(rowId);
+ }
+
+ @Override
+ public long getLong(int pos) {
+ return ((LongColumnVector) vector).getLong(rowId);
+ }
+
+ @Override
+ public float getFloat(int pos) {
+ return ((FloatColumnVector) vector).getFloat(rowId);
+ }
+
+ @Override
+ public double getDouble(int pos) {
+ return ((DoubleColumnVector) vector).getDouble(rowId);
+ }
+
+ @Override
+ public BinaryString getString(int pos) {
+ BytesColumnVector.Bytes bytes = ((BytesColumnVector)
vector).getBytes(rowId);
+ return BinaryString.fromBytes(bytes.data, bytes.offset, bytes.len);
+ }
+
+ @Override
+ public Decimal getDecimal(int pos, int precision, int scale) {
+ return ((DecimalColumnVector) vector).getDecimal(rowId, precision,
scale);
+ }
+
+ @Override
+ public Timestamp getTimestamp(int pos, int precision) {
+ return ((TimestampColumnVector) vector).getTimestamp(rowId,
precision);
+ }
+
+ @Override
+ public byte[] getBinary(int pos) {
+ return ((BytesColumnVector) vector).getBytes(rowId).getBytes();
+ }
+
+ @Override
+ public Variant getVariant(int pos) {
+ InternalRow row = getRow(pos, 2);
+ return new GenericVariant(row.getBinary(0), row.getBinary(1));
+ }
+
+ @Override
+ public Blob getBlob(int pos) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InternalArray getArray(int pos) {
+ return ((ArrayColumnVector) vector).getArray(rowId);
+ }
+
+ @Override
+ public InternalVector getVector(int pos) {
+ return ((VecColumnVector) vector).getVector(rowId);
+ }
+
+ @Override
+ public InternalMap getMap(int pos) {
+ return ((MapColumnVector) vector).getMap(rowId);
+ }
+
+ @Override
+ public InternalRow getRow(int pos, int numFields) {
+ return ((RowColumnVector) vector).getRow(rowId);
+ }
+ }
+
+ private static class ObjectDataGetters implements DataGetters {
+
+ private final Object value;
+
+ private ObjectDataGetters(Object value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean isNullAt(int pos) {
+ return value == null;
+ }
+
+ @Override
+ public boolean getBoolean(int pos) {
+ return (Boolean) value;
+ }
+
+ @Override
+ public byte getByte(int pos) {
+ return (Byte) value;
+ }
+
+ @Override
+ public short getShort(int pos) {
+ return (Short) value;
+ }
+
+ @Override
+ public int getInt(int pos) {
+ return (Integer) value;
+ }
+
+ @Override
+ public long getLong(int pos) {
+ return (Long) value;
+ }
+
+ @Override
+ public float getFloat(int pos) {
+ return (Float) value;
+ }
+
+ @Override
+ public double getDouble(int pos) {
+ return (Double) value;
+ }
+
+ @Override
+ public BinaryString getString(int pos) {
+ return (BinaryString) value;
+ }
+
+ @Override
+ public Decimal getDecimal(int pos, int precision, int scale) {
+ return (Decimal) value;
+ }
+
+ @Override
+ public Timestamp getTimestamp(int pos, int precision) {
+ return (Timestamp) value;
+ }
+
+ @Override
+ public byte[] getBinary(int pos) {
+ return (byte[]) value;
+ }
+
+ @Override
+ public Variant getVariant(int pos) {
+ return (Variant) value;
+ }
+
+ @Override
+ public Blob getBlob(int pos) {
+ return (Blob) value;
+ }
+
+ @Override
+ public InternalArray getArray(int pos) {
+ return (InternalArray) value;
+ }
+
+ @Override
+ public InternalVector getVector(int pos) {
+ return (InternalVector) value;
+ }
+
+ @Override
+ public InternalMap getMap(int pos) {
+ return (InternalMap) value;
+ }
+
+ @Override
+ public InternalRow getRow(int pos, int numFields) {
+ return (InternalRow) value;
+ }
+ }
+
private interface TypeConverter extends Serializable {
void append(DataGetters row, int column, WritableColumnVector cv);
@@ -107,20 +341,12 @@ public class RowToColumnConverter {
@Override
public TypeConverter visit(CharType charType) {
- return createConverter(
- charType.isNullable(),
- (row, column, cv) ->
- ((WritableByteVector)
cv).appendByte(row.getByte(column)));
+ return stringConverter(charType.isNullable());
}
@Override
public TypeConverter visit(VarCharType varCharType) {
- return createConverter(
- varCharType.isNullable(),
- (row, column, cv) -> {
- byte[] bytes = row.getString(column).toBytes();
- ((WritableBytesVector) cv).appendByteArray(bytes,
0, bytes.length);
- });
+ return stringConverter(varCharType.isNullable());
}
@Override
@@ -246,6 +472,7 @@ public class RowToColumnConverter {
return createConverter(
variantType.isNullable(),
(row, column, cv) -> {
+ ((HeapRowVector) cv).appendRow();
WritableBytesVector valueVector =
(WritableBytesVector) cv.getChildren()[0];
WritableBytesVector metaDataVector =
@@ -383,6 +610,15 @@ public class RowToColumnConverter {
});
}
+ private static TypeConverter stringConverter(boolean nullable) {
+ return createConverter(
+ nullable,
+ (row, column, cv) -> {
+ byte[] bytes = row.getString(column).toBytes();
+ ((WritableBytesVector) cv).appendByteArray(bytes,
0, bytes.length);
+ });
+ }
+
private static TypeConverter timestampConverter(boolean nullable,
int precision) {
return createConverter(
nullable,
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
index 6b5cce93d6..eb00505481 100644
---
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
@@ -24,6 +24,8 @@ 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_COMPRESSION =
+ "paimon.map.shared-shredding.field-dict-compression";
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";
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 851edb01d8..82f191f624 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
@@ -43,6 +43,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@@ -115,12 +116,14 @@ public class MapSharedShreddingUtils {
MapSharedShreddingFieldMeta fieldMeta,
@Nullable String compression,
Map<String, String> metadata) {
+ String fieldDictCompression =
normalizeFieldDictCompression(compression);
metadata.put(
MapShreddingDefine.STORAGE_LAYOUT,
MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
metadata.put(
MapSharedShreddingDefine.VERSION,
String.valueOf(MapSharedShreddingDefine.CURRENT_VERSION));
+ metadata.put(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION,
fieldDictCompression);
String fieldDictJson = toJson(new TreeMap<>(fieldMeta.nameToId()));
metadata.put(
@@ -129,7 +132,9 @@ public class MapSharedShreddingUtils {
metadata.put(
MapSharedShreddingDefine.FIELD_DICT,
bytesToString(
-
compress(fieldDictJson.getBytes(StandardCharsets.UTF_8), compression)));
+ compress(
+ fieldDictJson.getBytes(StandardCharsets.UTF_8),
+ fieldDictCompression)));
metadata.put(
MapSharedShreddingDefine.FIELD_COLUMNS,
toJson(sortedFieldColumns(fieldMeta.fieldToColumns())));
@@ -143,6 +148,18 @@ public class MapSharedShreddingUtils {
public static MapSharedShreddingFieldMeta deserializeMetadata(
@Nullable Map<String, String> metadata, @Nullable String
compression) {
+ return deserializeMetadata(metadata, compression, true);
+ }
+
+ public static MapSharedShreddingFieldMeta deserializeMetadata(
+ @Nullable Map<String, String> metadata) {
+ return deserializeMetadata(metadata, null, false);
+ }
+
+ private static MapSharedShreddingFieldMeta deserializeMetadata(
+ @Nullable Map<String, String> metadata,
+ @Nullable String fallbackCompression,
+ boolean useFallbackCompression) {
if (!hasShreddingMetadata(metadata)) {
throw new IllegalArgumentException(
"metadata is null or storage layout is not
shared-shredding");
@@ -158,6 +175,13 @@ public class MapSharedShreddingUtils {
int originalLength =
requiredInt(metadata,
MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE);
+ String compression =
+ normalizeFieldDictCompression(
+ metadata.getOrDefault(
+
MapSharedShreddingDefine.FIELD_DICT_COMPRESSION,
+ useFallbackCompression
+ ? fallbackCompression
+ :
MapSharedShreddingDefine.DEFAULT_DICT_COMPRESSION));
byte[] fieldDictBytes =
decompress(
stringToBytes(requiredValue(metadata,
MapSharedShreddingDefine.FIELD_DICT)),
@@ -272,6 +296,25 @@ public class MapSharedShreddingUtils {
return compression == null || "none".equalsIgnoreCase(compression);
}
+ public static String normalizeFieldDictCompression(@Nullable String
compression) {
+ if (compression == null) {
+ return MapSharedShreddingDefine.DEFAULT_DICT_COMPRESSION;
+ }
+
+ String normalized = compression.toLowerCase(Locale.ROOT);
+ switch (normalized) {
+ case "none":
+ case "lz4":
+ case "zstd":
+ return normalized;
+ default:
+ throw new IllegalArgumentException(
+ "MAP shared-shredding only supports none/lz4/zstd
compression, but is "
+ + compression
+ + ".");
+ }
+ }
+
private static String bytesToString(byte[] bytes) {
return new String(bytes, StandardCharsets.ISO_8859_1);
}
diff --git
a/paimon-common/src/test/java/org/apache/paimon/data/columnar/RowToColumnConverterTest.java
b/paimon-common/src/test/java/org/apache/paimon/data/columnar/RowToColumnConverterTest.java
index 95c20fc971..4318a54516 100644
---
a/paimon-common/src/test/java/org/apache/paimon/data/columnar/RowToColumnConverterTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/data/columnar/RowToColumnConverterTest.java
@@ -24,6 +24,9 @@ import org.apache.paimon.data.Decimal;
import org.apache.paimon.data.GenericArray;
import org.apache.paimon.data.GenericMap;
import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalArray;
+import org.apache.paimon.data.InternalMap;
+import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.InternalVector;
import org.apache.paimon.data.Timestamp;
import org.apache.paimon.data.columnar.heap.HeapArrayVector;
@@ -37,13 +40,16 @@ import org.apache.paimon.data.columnar.heap.HeapLongVector;
import org.apache.paimon.data.columnar.heap.HeapMapVector;
import org.apache.paimon.data.columnar.heap.HeapRowVector;
import org.apache.paimon.data.columnar.heap.HeapShortVector;
+import org.apache.paimon.data.columnar.heap.HeapTimestampVector;
import org.apache.paimon.data.columnar.heap.HeapVectorColumnVector;
import org.apache.paimon.data.columnar.writable.WritableColumnVector;
+import org.apache.paimon.data.variant.GenericVariant;
import org.apache.paimon.types.ArrayType;
import org.apache.paimon.types.BigIntType;
import org.apache.paimon.types.BinaryType;
import org.apache.paimon.types.BooleanType;
import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.DateType;
import org.apache.paimon.types.DecimalType;
@@ -62,8 +68,12 @@ import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
+import java.util.Arrays;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -233,6 +243,57 @@ public class RowToColumnConverterTest {
assertThat(new
String(bytesVector.getBytes(1).getBytes())).isEqualTo("world");
}
+ @Test
+ public void testConvertCharType() {
+ RowType rowType = RowType.of(new DataField(0, "f",
DataTypes.CHAR(10)));
+ RowToColumnConverter converter = new RowToColumnConverter(rowType);
+
+ GenericRow row1 = GenericRow.of(BinaryString.fromString("hello"));
+ GenericRow row2 = GenericRow.of(BinaryString.fromString("world"));
+
+ HeapBytesVector bytesVector = new HeapBytesVector(2);
+ WritableColumnVector[] vectors = new WritableColumnVector[]
{bytesVector};
+
+ converter.convert(row1, vectors);
+ converter.convert(row2, vectors);
+
+ assertThat(new
String(bytesVector.getBytes(0).getBytes())).isEqualTo("hello");
+ assertThat(new
String(bytesVector.getBytes(1).getBytes())).isEqualTo("world");
+ }
+
+ @Test
+ public void testElementConverterAppendFromObjectAndColumnVector() {
+ RowToColumnConverter.ElementConverter converter =
+
RowToColumnConverter.createElementConverter(DataTypes.STRING());
+ HeapBytesVector sourceVector = new HeapBytesVector(1);
+ sourceVector.appendByteArray("from-vector".getBytes(), 0,
"from-vector".length());
+
+ HeapBytesVector targetVector = new HeapBytesVector(2);
+ converter.append(BinaryString.fromString("from-object"), targetVector);
+ converter.append(sourceVector, 0, targetVector);
+
+ assertThat(new
String(targetVector.getBytes(0).getBytes())).isEqualTo("from-object");
+ assertThat(new
String(targetVector.getBytes(1).getBytes())).isEqualTo("from-vector");
+ }
+
+ @Test
+ public void testElementConverterSupportsAllSupportedTypes() {
+ for (ElementCase elementCase : elementCases()) {
+ elementCase.verify();
+ }
+ }
+
+ @Test
+ public void testElementConverterRejectsUnsupportedTypes() {
+ assertThatThrownBy(() ->
RowToColumnConverter.createElementConverter(DataTypes.BLOB()))
+ .isInstanceOf(UnsupportedOperationException.class);
+ assertThatThrownBy(
+ () ->
+ RowToColumnConverter.createElementConverter(
+ DataTypes.MULTISET(DataTypes.INT())))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
@Test
public void testConvertBinaryType() {
RowType rowType = RowType.of(new DataField(0, "f", new BinaryType(5)));
@@ -251,6 +312,23 @@ public class RowToColumnConverterTest {
assertThat(new
String(bytesVector.getBytes(1).getBytes())).isEqualTo("world");
}
+ @Test
+ public void testConvertVariantType() {
+ RowType rowType = RowType.of(new DataField(0, "f",
DataTypes.VARIANT()));
+ RowToColumnConverter converter = new RowToColumnConverter(rowType);
+ GenericVariant variant = GenericVariant.fromJson("{\"a\":1}");
+
+ HeapRowVector rowVector =
+ new HeapRowVector(1, new HeapBytesVector(1), new
HeapBytesVector(1));
+ WritableColumnVector[] vectors = new WritableColumnVector[]
{rowVector};
+
+ converter.convert(GenericRow.of(variant), vectors);
+
+ assertThat(rowVector.isNullAt(0)).isFalse();
+
assertThat(rowVector.getRow(0).getBinary(0)).isEqualTo(variant.value());
+
assertThat(rowVector.getRow(0).getBinary(1)).isEqualTo(variant.metadata());
+ }
+
@Test
public void testConvertVarBinaryType() {
RowType rowType = RowType.of(new DataField(0, "f", new
VarBinaryType(10)));
@@ -608,4 +686,314 @@ public class RowToColumnConverterTest {
});
return (InternalVector) proxy;
}
+
+ private static List<ElementCase> elementCases() {
+ Decimal smallDecimal = Decimal.fromBigDecimal(new BigDecimal("12.34"),
5, 2);
+ Decimal longDecimal = Decimal.fromBigDecimal(new
BigDecimal("1234567890.12"), 12, 2);
+ Decimal bytesDecimal =
+ Decimal.fromBigDecimal(new
BigDecimal("12345678901234567890.12"), 22, 2);
+ Timestamp millisTimestamp = Timestamp.fromEpochMillis(1234567890L);
+ Timestamp microsTimestamp = Timestamp.fromMicros(1234567890123456L);
+ Timestamp nanosTimestamp = Timestamp.fromEpochMillis(1234567890L,
123456);
+ GenericVariant variant = GenericVariant.fromJson("{\"a\":1}");
+
+ Map<BinaryString, Integer> map = new LinkedHashMap<>();
+ map.put(BinaryString.fromString("k1"), 1);
+ map.put(BinaryString.fromString("k2"), 2);
+
+ return Arrays.asList(
+ new ElementCase(
+ DataTypes.BOOLEAN(),
+ true,
+ () -> new HeapBooleanVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapBooleanVector)
vector).getBoolean(rowId))
+ .isTrue()),
+ new ElementCase(
+ DataTypes.TINYINT(),
+ (byte) 12,
+ () -> new HeapByteVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapByteVector)
vector).getByte(rowId))
+ .isEqualTo((byte) 12)),
+ new ElementCase(
+ DataTypes.SMALLINT(),
+ (short) 123,
+ () -> new HeapShortVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapShortVector)
vector).getShort(rowId))
+ .isEqualTo((short) 123)),
+ new ElementCase(
+ DataTypes.INT(),
+ 123,
+ () -> new HeapIntVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapIntVector)
vector).getInt(rowId)).isEqualTo(123)),
+ new ElementCase(
+ DataTypes.BIGINT(),
+ 123L,
+ () -> new HeapLongVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapLongVector)
vector).getLong(rowId))
+ .isEqualTo(123L)),
+ new ElementCase(
+ DataTypes.FLOAT(),
+ 1.25F,
+ () -> new HeapFloatVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapFloatVector)
vector).getFloat(rowId))
+ .isEqualTo(1.25F)),
+ new ElementCase(
+ DataTypes.DOUBLE(),
+ 2.5D,
+ () -> new HeapDoubleVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapDoubleVector)
vector).getDouble(rowId))
+ .isEqualTo(2.5D)),
+ new ElementCase(
+ DataTypes.DATE(),
+ 12345,
+ () -> new HeapIntVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapIntVector)
vector).getInt(rowId))
+ .isEqualTo(12345)),
+ new ElementCase(
+ DataTypes.TIME(),
+ 67890,
+ () -> new HeapIntVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapIntVector)
vector).getInt(rowId))
+ .isEqualTo(67890)),
+ new ElementCase(
+ DataTypes.CHAR(10),
+ BinaryString.fromString("char"),
+ () -> new HeapBytesVector(3),
+ (vector, rowId) ->
+ assertThat(bytes((HeapBytesVector) vector,
rowId))
+ .isEqualTo("char")),
+ new ElementCase(
+ DataTypes.VARCHAR(10),
+ BinaryString.fromString("varchar"),
+ () -> new HeapBytesVector(3),
+ (vector, rowId) ->
+ assertThat(bytes((HeapBytesVector) vector,
rowId))
+ .isEqualTo("varchar")),
+ new ElementCase(
+ DataTypes.BINARY(6),
+ "binary".getBytes(),
+ () -> new HeapBytesVector(3),
+ (vector, rowId) ->
+ assertThat(bytes((HeapBytesVector) vector,
rowId))
+ .isEqualTo("binary")),
+ new ElementCase(
+ DataTypes.VARBINARY(9),
+ "varbinary".getBytes(),
+ () -> new HeapBytesVector(3),
+ (vector, rowId) ->
+ assertThat(bytes((HeapBytesVector) vector,
rowId))
+ .isEqualTo("varbinary")),
+ new ElementCase(
+ DataTypes.DECIMAL(5, 2),
+ smallDecimal,
+ () -> new HeapIntVector(3),
+ decimalSource(smallDecimal),
+ (vector, rowId) ->
+ assertThat(((HeapIntVector)
vector).getInt(rowId))
+ .isEqualTo((int)
smallDecimal.toUnscaledLong())),
+ new ElementCase(
+ DataTypes.DECIMAL(12, 2),
+ longDecimal,
+ () -> new HeapLongVector(3),
+ decimalSource(longDecimal),
+ (vector, rowId) ->
+ assertThat(((HeapLongVector)
vector).getLong(rowId))
+
.isEqualTo(longDecimal.toUnscaledLong())),
+ new ElementCase(
+ DataTypes.DECIMAL(22, 2),
+ bytesDecimal,
+ () -> new HeapBytesVector(3),
+ decimalSource(bytesDecimal),
+ (vector, rowId) ->
+ assertThat(((HeapBytesVector)
vector).getBytes(rowId).getBytes())
+
.isEqualTo(bytesDecimal.toUnscaledBytes())),
+ new ElementCase(
+ DataTypes.TIMESTAMP(3),
+ millisTimestamp,
+ () -> new HeapLongVector(3),
+ timestampSource(millisTimestamp),
+ (vector, rowId) ->
+ assertThat(((HeapLongVector)
vector).getLong(rowId))
+
.isEqualTo(millisTimestamp.getMillisecond())),
+ new ElementCase(
+ DataTypes.TIMESTAMP(6),
+ microsTimestamp,
+ () -> new HeapLongVector(3),
+ timestampSource(microsTimestamp),
+ (vector, rowId) ->
+ assertThat(((HeapLongVector)
vector).getLong(rowId))
+
.isEqualTo(microsTimestamp.toMicros())),
+ new ElementCase(
+ DataTypes.TIMESTAMP(9),
+ nanosTimestamp,
+ () -> new HeapTimestampVector(3),
+ (vector, rowId) ->
+ assertThat(((HeapTimestampVector)
vector).getTimestamp(rowId, 9))
+ .isEqualTo(nanosTimestamp)),
+ new ElementCase(
+ DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3),
+ millisTimestamp,
+ () -> new HeapLongVector(3),
+ timestampSource(millisTimestamp),
+ (vector, rowId) ->
+ assertThat(((HeapLongVector)
vector).getLong(rowId))
+
.isEqualTo(millisTimestamp.getMillisecond())),
+ new ElementCase(
+ DataTypes.VARIANT(),
+ variant,
+ () -> new HeapRowVector(3, new HeapBytesVector(3), new
HeapBytesVector(3)),
+ (vector, rowId) -> {
+ InternalRow row = ((HeapRowVector)
vector).getRow(rowId);
+
assertThat(row.getBinary(0)).isEqualTo(variant.value());
+
assertThat(row.getBinary(1)).isEqualTo(variant.metadata());
+ }),
+ new ElementCase(
+ DataTypes.ARRAY(DataTypes.INT()),
+ new GenericArray(new Object[] {1, 2, 3}),
+ () -> new HeapArrayVector(3, new HeapIntVector(9)),
+ (vector, rowId) -> {
+ InternalArray array = ((HeapArrayVector)
vector).getArray(rowId);
+ assertThat(array.size()).isEqualTo(3);
+ assertThat(array.getInt(0)).isEqualTo(1);
+ assertThat(array.getInt(1)).isEqualTo(2);
+ assertThat(array.getInt(2)).isEqualTo(3);
+ }),
+ new ElementCase(
+ DataTypes.MAP(DataTypes.STRING(), DataTypes.INT()),
+ new GenericMap(map),
+ () -> new HeapMapVector(3, new HeapBytesVector(6), new
HeapIntVector(6)),
+ (vector, rowId) -> {
+ InternalMap internalMap = ((HeapMapVector)
vector).getMap(rowId);
+ assertThat(internalMap.size()).isEqualTo(2);
+
assertThat(internalMap.keyArray().getString(0).toString())
+ .isEqualTo("k1");
+
assertThat(internalMap.valueArray().getInt(0)).isEqualTo(1);
+
assertThat(internalMap.keyArray().getString(1).toString())
+ .isEqualTo("k2");
+
assertThat(internalMap.valueArray().getInt(1)).isEqualTo(2);
+ }),
+ new ElementCase(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "id", DataTypes.INT()),
+ DataTypes.FIELD(1, "name",
DataTypes.STRING())),
+ GenericRow.of(1, BinaryString.fromString("Alice")),
+ () -> new HeapRowVector(3, new HeapIntVector(3), new
HeapBytesVector(3)),
+ (vector, rowId) -> {
+ InternalRow row = ((HeapRowVector)
vector).getRow(rowId);
+ assertThat(row.getInt(0)).isEqualTo(1);
+
assertThat(row.getString(1).toString()).isEqualTo("Alice");
+ }),
+ new ElementCase(
+ DataTypes.VECTOR(3, DataTypes.FLOAT()),
+ BinaryVector.fromPrimitiveArray(new float[] {1.0F,
2.0F, 3.0F}),
+ () -> new HeapVectorColumnVector(3, new
HeapFloatVector(9), 3),
+ (vector, rowId) ->
+ assertThat(
+ ((HeapVectorColumnVector)
vector)
+ .getVector(rowId)
+ .toFloatArray())
+ .containsExactly(1.0F, 2.0F, 3.0F)));
+ }
+
+ private static String bytes(HeapBytesVector vector, int rowId) {
+ return new String(vector.getBytes(rowId).getBytes());
+ }
+
+ private static Supplier<ColumnVector> decimalSource(Decimal decimal) {
+ return () ->
+ new DecimalColumnVector() {
+ @Override
+ public boolean isNullAt(int i) {
+ return false;
+ }
+
+ @Override
+ public Decimal getDecimal(int i, int precision, int scale)
{
+ return decimal;
+ }
+ };
+ }
+
+ private static Supplier<ColumnVector> timestampSource(Timestamp timestamp)
{
+ return () ->
+ new TimestampColumnVector() {
+ @Override
+ public boolean isNullAt(int i) {
+ return false;
+ }
+
+ @Override
+ public Timestamp getTimestamp(int i, int precision) {
+ return timestamp;
+ }
+ };
+ }
+
+ private static class ElementCase {
+
+ private final DataType type;
+ private final Object value;
+ private final Supplier<WritableColumnVector> vectorSupplier;
+ private final Supplier<ColumnVector> sourceSupplier;
+ private final BiConsumer<ColumnVector, Integer> assertion;
+
+ private ElementCase(
+ DataType type,
+ Object value,
+ Supplier<WritableColumnVector> vectorSupplier,
+ BiConsumer<ColumnVector, Integer> assertion) {
+ this(
+ type,
+ value,
+ vectorSupplier,
+ () -> createSourceVector(type, value, vectorSupplier),
+ assertion);
+ }
+
+ private ElementCase(
+ DataType type,
+ Object value,
+ Supplier<WritableColumnVector> vectorSupplier,
+ Supplier<ColumnVector> sourceSupplier,
+ BiConsumer<ColumnVector, Integer> assertion) {
+ this.type = type;
+ this.value = value;
+ this.vectorSupplier = vectorSupplier;
+ this.sourceSupplier = sourceSupplier;
+ this.assertion = assertion;
+ }
+
+ private void verify() {
+ RowToColumnConverter.ElementConverter converter =
+ RowToColumnConverter.createElementConverter(type);
+
+ WritableColumnVector objectTarget = vectorSupplier.get();
+ converter.append(value, objectTarget);
+ assertion.accept(objectTarget, 0);
+
+ WritableColumnVector vectorTarget = vectorSupplier.get();
+ converter.append(sourceSupplier.get(), 0, vectorTarget);
+ assertion.accept(vectorTarget, 0);
+
+ WritableColumnVector nullTarget = vectorSupplier.get();
+ converter.append(null, nullTarget);
+ assertThat(nullTarget.isNullAt(0)).isTrue();
+ }
+
+ private static ColumnVector createSourceVector(
+ DataType type, Object value, Supplier<WritableColumnVector>
vectorSupplier) {
+ WritableColumnVector source = vectorSupplier.get();
+ RowToColumnConverter.createElementConverter(type).append(value,
source);
+ return source;
+ }
+ }
}
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 e9d8d03f09..9c86109efa 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
@@ -204,7 +204,7 @@ class MapSharedShreddingUtilsTest {
new MapSharedShreddingFieldMeta(nameToId, fieldToColumns,
overflowSet, 3, 2);
String expectedDict = "{\"age\":0,\"name\":1}";
- for (String compression : Arrays.<String>asList(null, "none", "NONE"))
{
+ for (String compression : Arrays.asList("none", "NONE")) {
Map<String, String> metadata = new HashMap<>();
MapSharedShreddingUtils.serializeMetadata(original, compression,
metadata);
@@ -212,6 +212,8 @@ class MapSharedShreddingUtilsTest {
assertThat(metadata.get(MapShreddingDefine.STORAGE_LAYOUT))
.isEqualTo("shared-shredding");
assertThat(metadata.get(MapSharedShreddingDefine.VERSION)).isEqualTo("1");
+
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION))
+ .isEqualTo("none");
assertThat(metadata.get(MapSharedShreddingDefine.NUM_COLUMNS)).isEqualTo("3");
assertThat(metadata.get(MapSharedShreddingDefine.MAX_ROW_WIDTH)).isEqualTo("2");
@@ -221,9 +223,14 @@ class MapSharedShreddingUtilsTest {
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);
+
assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata)).isEqualTo(original);
}
+
+ Map<String, String> metadataWithoutCompression = new HashMap<>();
+ MapSharedShreddingUtils.serializeMetadata(original, "zstd",
metadataWithoutCompression);
+
metadataWithoutCompression.remove(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION);
+
assertThat(MapSharedShreddingUtils.deserializeMetadata(metadataWithoutCompression))
+ .isEqualTo(original);
}
@Test
@@ -246,8 +253,9 @@ class MapSharedShreddingUtilsTest {
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);
+
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION))
+ .isEqualTo(compression);
+
assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata)).isEqualTo(original);
}
}
@@ -260,32 +268,56 @@ class MapSharedShreddingUtilsTest {
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);
+
assertThat(metadata.get(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION))
+ .isEqualTo(compression);
+
assertThat(MapSharedShreddingUtils.deserializeMetadata(metadata)).isEqualTo(original);
}
}
+ @Test
+ void testMetadataRejectsUnknownCompression() {
+ Map<String, Integer> nameToId = new TreeMap<>();
+ nameToId.put("age", 0);
+ MapSharedShreddingFieldMeta original =
+ new MapSharedShreddingFieldMeta(nameToId, new TreeMap<>(), new
HashSet<>(), 1, 1);
+
+ assertThatThrownBy(
+ () ->
+ MapSharedShreddingUtils.serializeMetadata(
+ original, "snappy", new HashMap<>()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "MAP shared-shredding only supports none/lz4/zstd
compression");
+
+ Map<String, String> metadata = new HashMap<>();
+ MapSharedShreddingUtils.serializeMetadata(original, "zstd", metadata);
+ metadata.put(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION,
"snappy");
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(metadata))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "MAP shared-shredding only supports none/lz4/zstd
compression");
+ }
+
@Test
void testDeserializeMetadataErrors() {
- assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(null, "none"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(null))
.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"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(missingLayout))
.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"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(metadata))
.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"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(missingVersion))
.hasMessageContaining(
"missing shredding metadata key:
paimon.map.shared-shredding.version");
@@ -296,7 +328,7 @@ class MapSharedShreddingUtilsTest {
wrongVersion.put(MapSharedShreddingDefine.VERSION, "999");
wrongVersion.put(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE,
"2");
wrongVersion.put(MapSharedShreddingDefine.FIELD_DICT, "{}");
- assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(wrongVersion, "none"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(wrongVersion))
.hasMessageContaining("unsupported shared-shredding metadata
version: 999");
Map<String, String> missingFieldDict = new HashMap<>();
@@ -305,8 +337,7 @@ class MapSharedShreddingUtilsTest {
MapShreddingDefine.STORAGE_LAYOUT_SHARED_SHREDDING);
missingFieldDict.put(MapSharedShreddingDefine.VERSION, "1");
missingFieldDict.put(MapSharedShreddingDefine.FIELD_DICT_ORIGINAL_SIZE, "2");
- assertThatThrownBy(
- () ->
MapSharedShreddingUtils.deserializeMetadata(missingFieldDict, "none"))
+ assertThatThrownBy(() ->
MapSharedShreddingUtils.deserializeMetadata(missingFieldDict))
.hasMessageContaining(
"missing shredding metadata key:
paimon.map.shared-shredding.field-dict");
}
diff --git
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
index 69010c2594..2cfde7d648 100644
---
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingWritePlanTest.java
@@ -65,8 +65,10 @@ class MapSharedShreddingWritePlanTest {
Map<String, Map<String, String>> fieldMetadata =
writePlan.fieldMetadata("none");
assertThat(fieldMetadata).containsOnlyKeys("tags");
+ assertThat(fieldMetadata.get("tags"))
+
.containsEntry(MapSharedShreddingDefine.FIELD_DICT_COMPRESSION, "none");
MapSharedShreddingFieldMeta fieldMeta =
-
MapSharedShreddingUtils.deserializeMetadata(fieldMetadata.get("tags"), "none");
+
MapSharedShreddingUtils.deserializeMetadata(fieldMetadata.get("tags"));
assertThat(fieldMeta.nameToId()).containsEntry("a",
0).containsEntry("b", 1);
assertThat(fieldMeta.numColumns()).isEqualTo(4);
assertThat(fieldMeta.maxRowWidth()).isEqualTo(3);
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 491da71ff1..56b4676dca 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
@@ -45,6 +45,7 @@ import org.apache.paimon.types.MapType;
import org.apache.paimon.types.MultisetType;
import org.apache.paimon.types.RowType;
import org.apache.paimon.types.TimestampType;
+import org.apache.paimon.types.VariantType;
import org.apache.paimon.types.VectorType;
import org.apache.paimon.utils.Preconditions;
import org.apache.paimon.utils.SetUtils;
@@ -59,6 +60,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.apache.paimon.CoreOptions.BUCKET_KEY;
@@ -350,6 +352,8 @@ public class SchemaValidation {
validateMergeFunctionFactory(schema);
+ validateMapStorageLayout(schema, options);
+
validateFileIndex(schema);
validateRowTracking(schema, options);
@@ -363,8 +367,6 @@ public class SchemaValidation {
validatePkClusteringOverride(options);
validateManifestSort(schema, options);
-
- validateMapStorageLayout(schema, options);
}
public static void validateFallbackBranch(SchemaManager schemaManager,
TableSchema schema) {
@@ -621,6 +623,7 @@ public class SchemaValidation {
fieldMap.put(field.name(), field);
}
+ boolean hasSharedShredding = false;
for (String key : options.toMap().keySet()) {
if (!key.startsWith(FIELDS_PREFIX + ".") ||
!key.endsWith(layoutSuffix)) {
continue;
@@ -650,6 +653,7 @@ public class SchemaValidation {
continue;
}
+ hasSharedShredding = true;
if (!MapSharedShreddingUtils.isShreddingKeyMap(fieldType)) {
throw new IllegalArgumentException(
String.format(
@@ -658,6 +662,119 @@ public class SchemaValidation {
}
options.mapSharedShreddingMaxColumns(fieldName);
}
+
+ if (hasSharedShredding) {
+ validateMapSharedShreddingFileFormats(options);
+ validateMapSharedShreddingCompressions(options);
+ validateUnsupportedTypesWithMapSharedShredding(schema);
+ if (!schema.primaryKeys().isEmpty()) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently only supports
append-only tables.");
+ }
+ if (options.bucket() != -1 && !options.writeOnly()) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently requires bucket = -1
or write-only = true because rewrite/compaction is not supported.");
+ }
+ }
+ }
+
+ private static void
validateUnsupportedTypesWithMapSharedShredding(TableSchema schema) {
+ RowType rowType = new RowType(schema.fields());
+ if (containsType(rowType, type -> type instanceof VariantType)) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently cannot be used with
Variant fields.");
+ }
+ if (containsType(rowType, type -> type.is(DataTypeRoot.BLOB))) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently cannot be used with BLOB
fields.");
+ }
+ if (containsType(rowType, type -> type instanceof MultisetType)) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently cannot be used with
MULTISET fields.");
+ }
+ if (containsType(rowType, type -> type instanceof VectorType)) {
+ throw new IllegalArgumentException(
+ "MAP shared-shredding currently cannot be used with VECTOR
fields.");
+ }
+ }
+
+ private static boolean containsType(DataType dataType, Predicate<DataType>
predicate) {
+ if (predicate.test(dataType)) {
+ return true;
+ }
+ if (dataType instanceof RowType) {
+ for (DataField field : ((RowType) dataType).getFields()) {
+ if (containsType(field.type(), predicate)) {
+ return true;
+ }
+ }
+ } else if (dataType instanceof ArrayType) {
+ return containsType(((ArrayType) dataType).getElementType(),
predicate);
+ } else if (dataType instanceof MultisetType) {
+ return containsType(((MultisetType) dataType).getElementType(),
predicate);
+ } else if (dataType instanceof MapType) {
+ MapType mapType = (MapType) dataType;
+ return containsType(mapType.getKeyType(), predicate)
+ || containsType(mapType.getValueType(), predicate);
+ } else if (dataType instanceof VectorType) {
+ return containsType(((VectorType) dataType).getElementType(),
predicate);
+ }
+ return false;
+ }
+
+ private static void validateMapSharedShreddingFileFormats(CoreOptions
options) {
+ validateMapSharedShreddingFileFormat(
+ CoreOptions.FILE_FORMAT.key(), options.fileFormatString());
+ for (Map.Entry<Integer, String> entry :
options.fileFormatPerLevel().entrySet()) {
+ validateMapSharedShreddingFileFormat(
+ CoreOptions.FILE_FORMAT_PER_LEVEL.key() + "." +
entry.getKey(),
+ entry.getValue());
+ }
+ validateMapSharedShreddingFileFormat(
+ CoreOptions.CHANGELOG_FILE_FORMAT.key(),
options.changelogFileFormat());
+ validateMapSharedShreddingFileFormat(
+ CoreOptions.VECTOR_FILE_FORMAT.key(),
options.vectorFileFormatString());
+ }
+
+ private static void validateMapSharedShreddingCompressions(CoreOptions
options) {
+ validateMapSharedShreddingCompression(
+ CoreOptions.FILE_COMPRESSION.key(), options.fileCompression());
+ for (Map.Entry<Integer, String> entry :
options.fileCompressionPerLevel().entrySet()) {
+ validateMapSharedShreddingCompression(
+ CoreOptions.FILE_COMPRESSION_PER_LEVEL.key() + "." +
entry.getKey(),
+ entry.getValue());
+ }
+ validateMapSharedShreddingCompression(
+ CoreOptions.CHANGELOG_FILE_COMPRESSION.key(),
options.changelogFileCompression());
+ }
+
+ private static void validateMapSharedShreddingCompression(
+ String optionKey, String compression) {
+ if (StringUtils.isEmpty(compression)) {
+ return;
+ }
+ try {
+ MapSharedShreddingUtils.normalizeFieldDictCompression(compression);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "MAP shared-shredding only supports none/lz4/zstd
compression, but %s is %s.",
+ optionKey, compression),
+ e);
+ }
+ }
+
+ private static void validateMapSharedShreddingFileFormat(String optionKey,
String format) {
+ if (StringUtils.isEmpty(format)) {
+ return;
+ }
+ if (!CoreOptions.FILE_FORMAT_PARQUET.equals(format)
+ && !CoreOptions.FILE_FORMAT_ORC.equals(format)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "MAP shared-shredding only supports parquet/orc
file formats, but %s is %s.",
+ optionKey, format));
+ }
}
private static void validateFileIndex(TableSchema schema) {
@@ -700,6 +817,11 @@ public class SchemaValidation {
+ "Only CHAR/VARCHAR/STRING is supported.",
columnName,
keyType);
+ checkArgument(
+ options.mapStorageLayout(columnName) !=
MapStorageLayout.SHARED_SHREDDING,
+ "Column '%s' is configured with
map.storage-layout=shared-shredding, "
+ + "which does not support nested file index.",
+ columnName);
}
for (String indexType : entry.getValue().keySet()) {
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 f08c852c97..c1d673b168 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
@@ -20,6 +20,7 @@ package org.apache.paimon.schema;
import org.apache.paimon.CoreOptions;
import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.assertj.core.api.ThrowableAssert;
@@ -313,6 +314,192 @@ class SchemaValidationTest {
.hasMessageContaining("options
map.shared-shredding.max-columns must > 0");
}
+ @Test
+ public void testMapSharedShreddingCannotCombineWithUnsupportedTypes() {
+ List<DataField> variantFields =
topLevelPayloadFields(DataTypes.VARIANT());
+ String variantMessage =
+ "MAP shared-shredding currently cannot be used with Variant
fields.";
+ assertMapSharedShreddingValidationFailed(
+ variantFields, mapSharedShreddingOptions(), variantMessage);
+
+ Map<String, String> inferVariantOptions = mapSharedShreddingOptions();
+
inferVariantOptions.put(CoreOptions.VARIANT_INFER_SHREDDING_SCHEMA.key(),
"true");
+ assertMapSharedShreddingValidationFailed(
+ variantFields, inferVariantOptions, variantMessage);
+
+ Map<String, String> explicitVariantOptions =
mapSharedShreddingOptions();
+ explicitVariantOptions.put(CoreOptions.VARIANT_SHREDDING_SCHEMA.key(),
"{}");
+ assertMapSharedShreddingValidationFailed(
+ variantFields, explicitVariantOptions, variantMessage);
+
+ assertMapSharedShreddingValidationFailed(
+ mapValueFields(DataTypes.BLOB()),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with BLOB
fields.");
+ assertMapSharedShreddingValidationFailed(
+ nestedMapValueFields(DataTypes.BLOB()),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with BLOB
fields.");
+ assertMapSharedShreddingValidationFailed(
+ topLevelPayloadFields(DataTypes.BLOB()),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with BLOB
fields.");
+
+ DataType multisetType = DataTypes.MULTISET(DataTypes.INT());
+ assertMapSharedShreddingValidationFailed(
+ mapValueFields(multisetType),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with MULTISET
fields.");
+ assertMapSharedShreddingValidationFailed(
+ nestedMapValueFields(multisetType),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with MULTISET
fields.");
+ assertMapSharedShreddingValidationFailed(
+ topLevelPayloadFields(multisetType),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with MULTISET
fields.");
+
+ DataType vectorType = DataTypes.VECTOR(3, DataTypes.FLOAT());
+ assertMapSharedShreddingValidationFailed(
+ mapValueFields(vectorType),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with VECTOR
fields.");
+ assertMapSharedShreddingValidationFailed(
+ topLevelPayloadFields(vectorType),
+ mapSharedShreddingOptions(),
+ "MAP shared-shredding currently cannot be used with VECTOR
fields.");
+ }
+
+ @Test
+ public void testMapSharedShreddingFileFormatValidation() {
+ Map<String, String> fileFormatOptions = mapSharedShreddingOptions();
+ fileFormatOptions.put(CoreOptions.FILE_FORMAT.key(), "avro");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+
mapSharedShreddingSchema(fileFormatOptions, emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports parquet/orc file
formats, but file.format is avro.");
+
+ Map<String, String> levelFormatOptions = mapSharedShreddingOptions();
+ levelFormatOptions.put(CoreOptions.FILE_FORMAT_PER_LEVEL.key(),
"0:avro");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+
mapSharedShreddingSchema(levelFormatOptions, emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports parquet/orc file
formats, but file.format.per.level.0 is avro.");
+
+ Map<String, String> changelogFormatOptions =
mapSharedShreddingOptions();
+ changelogFormatOptions.put(CoreOptions.CHANGELOG_FILE_FORMAT.key(),
"avro");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ changelogFormatOptions,
emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports parquet/orc file
formats, but changelog-file.format is avro.");
+
+ Map<String, String> vectorFormatOptions = mapSharedShreddingOptions();
+ vectorFormatOptions.put(DATA_EVOLUTION_ENABLED.key(), "true");
+ vectorFormatOptions.put(CoreOptions.ROW_TRACKING_ENABLED.key(),
"true");
+ vectorFormatOptions.put(CoreOptions.VECTOR_FILE_FORMAT.key(), "json");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+
mapSharedShreddingSchema(vectorFormatOptions, emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports parquet/orc file
formats, but vector.file.format is json.");
+ }
+
+ @Test
+ public void testMapSharedShreddingFileCompressionValidation() {
+ Map<String, String> fileCompressionOptions =
mapSharedShreddingOptions();
+ fileCompressionOptions.put(CoreOptions.FILE_COMPRESSION.key(),
"snappy");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ fileCompressionOptions,
emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports none/lz4/zstd
compression, but file.compression is snappy.");
+
+ Map<String, String> levelCompressionOptions =
mapSharedShreddingOptions();
+
levelCompressionOptions.put(CoreOptions.FILE_COMPRESSION_PER_LEVEL.key(),
"0:snappy");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ levelCompressionOptions,
emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports none/lz4/zstd
compression, but file.compression.per.level.0 is snappy.");
+
+ Map<String, String> changelogCompressionOptions =
mapSharedShreddingOptions();
+
changelogCompressionOptions.put(CoreOptions.CHANGELOG_FILE_COMPRESSION.key(),
"snappy");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ changelogCompressionOptions,
emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding only supports none/lz4/zstd
compression, but changelog-file.compression is snappy.");
+
+ for (String compression : Arrays.asList("none", "lz4", "zstd",
"ZSTD")) {
+ Map<String, String> supportedOptions = mapSharedShreddingOptions();
+ supportedOptions.put(CoreOptions.FILE_COMPRESSION.key(),
compression);
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ supportedOptions,
emptyList())));
+ }
+ }
+
+ @Test
+ public void testMapSharedShreddingTableModeValidation() {
+ Map<String, String> primaryKeyOptions = mapSharedShreddingOptions();
+ primaryKeyOptions.put(BUCKET.key(), "-1");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ mapSharedShreddingSchema(
+ primaryKeyOptions,
singletonList("id"))))
+ .hasMessageContaining(
+ "MAP shared-shredding currently only supports
append-only tables.");
+
+ Map<String, String> fixedBucketOptions = mapSharedShreddingOptions();
+ fixedBucketOptions.put(BUCKET.key(), "1");
+ fixedBucketOptions.put(CoreOptions.BUCKET_KEY.key(), "id");
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+
mapSharedShreddingSchema(fixedBucketOptions, emptyList())))
+ .hasMessageContaining(
+ "MAP shared-shredding currently requires bucket = -1
or write-only = true because rewrite/compaction is not supported.");
+
+ Map<String, String> writeOnlyOptions = mapSharedShreddingOptions();
+ writeOnlyOptions.put(BUCKET.key(), "1");
+ writeOnlyOptions.put(CoreOptions.BUCKET_KEY.key(), "id");
+ writeOnlyOptions.put(CoreOptions.WRITE_ONLY.key(), "true");
+ assertThatNoException()
+ .isThrownBy(
+ () ->
+ validateTableSchema(
+
mapSharedShreddingSchema(writeOnlyOptions, emptyList())));
+ }
+
+ @Test
+ public void testMapSharedShreddingNestedFileIndexValidation() {
+ Map<String, String> options = mapSharedShreddingOptions();
+ options.put("file-index.bloom-filter.columns", "metrics[k]");
+
+ assertThatThrownBy(
+ () ->
validateTableSchema(mapSharedShreddingSchema(options, emptyList())))
+ .hasMessageContaining(
+ "Column 'metrics' is configured with
map.storage-layout=shared-shredding, which does not support nested file
index.");
+ }
+
@Test
public void testChainTableAllowsNonDeduplicateMergeEngine() {
Map<String, String> options = new HashMap<>();
@@ -1041,6 +1228,60 @@ class SchemaValidationTest {
assertThatCode(() ->
validateTableSchemaExec(options)).doesNotThrowAnyException();
}
+ private Map<String, String> mapSharedShreddingOptions() {
+ Map<String, String> options = new HashMap<>();
+ options.put(BUCKET.key(), "-1");
+ options.put(CoreOptions.FILE_FORMAT.key(), "parquet");
+ options.put("fields.metrics.map.storage-layout", "shared-shredding");
+ return options;
+ }
+
+ private TableSchema mapSharedShreddingSchema(
+ Map<String, String> options, List<String> primaryKeys) {
+ return new TableSchema(
+ 1, mapValueFields(DataTypes.INT()), 10, emptyList(),
primaryKeys, options, "");
+ }
+
+ private void assertMapSharedShreddingValidationFailed(
+ List<DataField> fields, Map<String, String> options, String
message) {
+ assertThatThrownBy(
+ () ->
+ validateTableSchema(
+ new TableSchema(
+ 1,
+ fields,
+ 10,
+ emptyList(),
+ emptyList(),
+ options,
+ "")))
+ .hasMessageContaining(message);
+ }
+
+ private List<DataField> mapValueFields(DataType valueType) {
+ return Arrays.asList(
+ new DataField(0, "id", DataTypes.INT()),
+ new DataField(1, "metrics", DataTypes.MAP(DataTypes.STRING(),
valueType)));
+ }
+
+ private List<DataField> nestedMapValueFields(DataType valueType) {
+ return Arrays.asList(
+ new DataField(0, "id", DataTypes.INT()),
+ new DataField(
+ 1,
+ "metrics",
+ DataTypes.MAP(
+ DataTypes.STRING(),
+ DataTypes.ROW(DataTypes.FIELD(0, "payload",
valueType)))));
+ }
+
+ private List<DataField> topLevelPayloadFields(DataType payloadType) {
+ return Arrays.asList(
+ new DataField(0, "id", DataTypes.INT()),
+ new DataField(1, "metrics", DataTypes.MAP(DataTypes.STRING(),
DataTypes.INT())),
+ new DataField(2, "payload", payloadType));
+ }
+
private TableSchema vectorTypeSchema(
List<String> partitionKeys, List<String> primaryKeys, List<String>
upsertKeys) {
List<DataField> fields =