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 877c9e42e3 [core][spark] Support selected-key pushdown for
shared-shredding MAP (#8782)
877c9e42e3 is described below
commit 877c9e42e3bb6f6477ff3a17f5c55194f73c2f7d
Author: lszskye <[email protected]>
AuthorDate: Tue Jul 28 02:44:23 2026 -0700
[core][spark] Support selected-key pushdown for shared-shredding MAP (#8782)
---
.../data/shredding/MapSharedShreddingReadPlan.java | 342 ++++++++++++++++++---
.../data/shredding/MapSharedShreddingUtils.java | 102 ++++--
.../shredding/MapSharedShreddingReadPlanTest.java | 125 ++++++++
.../shredding/MapSharedShreddingUtilsTest.java | 28 ++
.../org/apache/paimon/schema/SchemaManager.java | 46 +++
.../apache/paimon/utils/FormatReaderMapping.java | 73 ++++-
.../compact/aggregate/TestMapOnlyAggFactory.java | 46 +++
.../apache/paimon/schema/SchemaManagerTest.java | 92 ++++++
.../paimon/table/MapSharedShreddingTableTest.java | 212 ++++++++++---
.../paimon/utils/FormatReaderMappingTest.java | 217 +++++++++++++
.../services/org.apache.paimon.factories.Factory | 3 +-
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../MapSelectedKeysSharedShreddingE2ETest.scala | 21 ++
.../optimizer/PushDownMapSelectedKeys.scala | 11 +-
.../paimon/spark/execution/PaimonStrategy.scala | 15 +-
.../extensions/PaimonSparkSessionExtensions.scala | 2 -
...MapSelectedKeysSharedShreddingE2ETestBase.scala | 215 +++++++++++++
21 files changed, 1537 insertions(+), 118 deletions(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlan.java
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlan.java
index 521dbd5b55..f09c58d685 100644
---
a/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlan.java
+++
b/paimon-common/src/main/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlan.java
@@ -30,14 +30,17 @@ import org.apache.paimon.data.columnar.RowToColumnConverter;
import org.apache.paimon.data.columnar.VectorizedColumnBatch;
import org.apache.paimon.data.columnar.heap.CastedMapColumnVector;
import org.apache.paimon.data.columnar.heap.HeapMapVector;
+import org.apache.paimon.data.columnar.heap.HeapRowVector;
import org.apache.paimon.data.columnar.writable.WritableColumnVector;
import org.apache.paimon.types.DataField;
-import org.apache.paimon.types.DataType;
import org.apache.paimon.types.MapType;
import org.apache.paimon.types.RowType;
+import javax.annotation.Nullable;
+
import java.util.Arrays;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import static org.apache.paimon.utils.Preconditions.checkArgument;
@@ -45,6 +48,8 @@ import static
org.apache.paimon.utils.Preconditions.checkArgument;
/** Read plan that rebuilds logical MAP values from shared-shredding physical
ROW values. */
public class MapSharedShreddingReadPlan implements ShreddingReadPlan {
+ private static final int FIELD_MAPPING_POSITION = 0;
+
private final RowType logicalType;
private final RowType physicalType;
private final Map<Integer, SharedShreddingContext> contextByFieldIndex;
@@ -53,7 +58,7 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
RowType logicalType, Map<String, MapSharedShreddingFieldMeta>
fieldMetas) {
this.logicalType = logicalType;
this.physicalType =
MapSharedShreddingUtils.buildPhysicalReadType(logicalType, fieldMetas);
- this.contextByFieldIndex = createContexts(logicalType, fieldMetas);
+ this.contextByFieldIndex = createContexts(logicalType, physicalType,
fieldMetas);
}
@Override
@@ -77,13 +82,30 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
}
private static Map<Integer, SharedShreddingContext> createContexts(
- RowType logicalType, Map<String, MapSharedShreddingFieldMeta>
fieldMetas) {
+ RowType logicalType,
+ RowType physicalType,
+ Map<String, MapSharedShreddingFieldMeta> fieldMetas) {
Map<Integer, SharedShreddingContext> contexts = new LinkedHashMap<>();
for (int i = 0; i < logicalType.getFieldCount(); i++) {
DataField field = logicalType.getFields().get(i);
MapSharedShreddingFieldMeta fieldMeta =
fieldMetas.get(field.name());
- if (fieldMeta != null && field.type() instanceof MapType) {
- contexts.put(i, new SharedShreddingContext(fieldMeta,
field.type()));
+ if (fieldMeta == null) {
+ continue;
+ }
+
+ RowType physicalStructType = (RowType) physicalType.getTypeAt(i);
+ SharedPhysicalContext physicalContext =
+ new SharedPhysicalContext(fieldMeta, physicalStructType);
+ if (field.type() instanceof MapType) {
+ contexts.put(i, new FullMapContext(physicalContext, (MapType)
field.type()));
+ } else if
(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(field)) {
+ contexts.put(
+ i,
+ new SelectedKeysContext(
+ physicalContext,
+ fieldMeta,
+ (RowType) field.type(),
+ field.description()));
}
}
return contexts;
@@ -100,9 +122,8 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
logicalVectors[i] = physicalBatch.columns[i];
} else {
logicalVectors[i] =
- materializeLogicalMapVector(
+ context.materialize(
(RowColumnVector) physicalBatch.columns[i],
- context,
physicalBatch.getNumRows());
}
}
@@ -111,7 +132,7 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
}
private static CastedMapColumnVector materializeLogicalMapVector(
- RowColumnVector physicalVector, SharedShreddingContext context,
int rowCount) {
+ RowColumnVector physicalVector, FullMapContext context, int
rowCount) {
ColumnVector[] physicalChildren = physicalChildren(physicalVector);
int totalElements =
countLogicalMapElements(physicalVector, physicalChildren,
context, rowCount);
@@ -147,6 +168,46 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
new WritableColumnVector[] {keyVector, valueVector}));
}
+ private static ColumnVector materializeSelectedKeysRowVector(
+ RowColumnVector physicalVector, SelectedKeysContext context, int
rowCount) {
+ ColumnVector[] physicalChildren = physicalChildren(physicalVector);
+ RowType selectedKeysType = context.selectedKeysType;
+ WritableColumnVector[] selectedKeyVectors =
+ new WritableColumnVector[selectedKeysType.getFieldCount()];
+ for (int i = 0; i < selectedKeyVectors.length; i++) {
+ selectedKeyVectors[i] =
+ ColumnVectorUtils.createWritableColumnVector(
+ rowCount, selectedKeysType.getTypeAt(i));
+ }
+ HeapRowVector rowVector = new HeapRowVector(rowCount,
selectedKeyVectors);
+
+ for (int row = 0; row < rowCount; row++) {
+ if (physicalVector.isNullAt(row)) {
+ rowVector.appendNull();
+ continue;
+ }
+
+ InternalArray fieldMapping = fieldMapping(physicalChildren, row,
context.physical);
+ InternalMap overflow = null;
+ for (int ordinal = 0; ordinal < selectedKeyVectors.length;
ordinal++) {
+ if (context.selectedKeyOverflow[ordinal] && overflow == null) {
+ overflow = overflowMap(physicalChildren, row,
context.physical);
+ }
+ appendSelectedKeyValue(
+ physicalChildren,
+ fieldMapping,
+ context.selectedKeyOverflow[ordinal] ? overflow : null,
+ row,
+ context,
+ ordinal,
+ selectedKeyVectors[ordinal]);
+ }
+ rowVector.appendRow();
+ }
+
+ return ColumnVectorUtils.createReadableColumnVector(selectedKeysType,
rowVector);
+ }
+
private static ColumnVector[] physicalChildren(RowColumnVector
physicalVector) {
ColumnVector[] physicalChildren = physicalVector.getChildren();
if (physicalChildren != null) {
@@ -158,7 +219,7 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
private static int countLogicalMapElements(
RowColumnVector physicalVector,
ColumnVector[] physicalChildren,
- SharedShreddingContext context,
+ FullMapContext context,
int rowCount) {
int totalElements = 0;
for (int row = 0; row < rowCount; row++) {
@@ -170,20 +231,20 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
}
private static int countLogicalMapElements(
- ColumnVector[] physicalChildren, int row, SharedShreddingContext
context) {
+ ColumnVector[] physicalChildren, int row, FullMapContext context) {
int count = 0;
- InternalArray fieldMapping = fieldMapping(physicalChildren, row,
context);
- for (int column = 0; column < context.numColumns; column++) {
- if (logicalFieldName(fieldMapping, column, context) != null) {
+ InternalArray fieldMapping = fieldMapping(physicalChildren, row,
context.physical);
+ for (int column = 0; column < context.physical.numColumns; column++) {
+ if (logicalFieldName(fieldMapping, column, context.physical) !=
null) {
count++;
}
}
- InternalMap overflow = overflowMap(physicalChildren, row, context);
+ InternalMap overflow = overflowMap(physicalChildren, row,
context.physical);
if (overflow != null) {
InternalArray keys = overflow.keyArray();
for (int i = 0; i < overflow.size(); i++) {
- if (context.nameById.containsKey(keys.getInt(i))) {
+ if (context.physical.nameById.containsKey(keys.getInt(i))) {
count++;
}
}
@@ -194,32 +255,32 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
private static int appendLogicalMapElements(
ColumnVector[] physicalChildren,
int row,
- SharedShreddingContext context,
+ FullMapContext context,
WritableColumnVector keyVector,
WritableColumnVector valueVector) {
int count = 0;
- InternalArray fieldMapping = fieldMapping(physicalChildren, row,
context);
- for (int column = 0; column < context.numColumns; column++) {
- BinaryString fieldName = logicalFieldName(fieldMapping, column,
context);
+ InternalArray fieldMapping = fieldMapping(physicalChildren, row,
context.physical);
+ for (int column = 0; column < context.physical.numColumns; column++) {
+ BinaryString fieldName = logicalFieldName(fieldMapping, column,
context.physical);
if (fieldName == null) {
continue;
}
context.keyConverter.append(fieldName, keyVector);
- ColumnVector valueColumn = physicalChildren[column + 1];
- appendValue(context, valueColumn, row, valueVector);
+ ColumnVector valueColumn = physicalColumn(physicalChildren,
column, context.physical);
+ appendValue(context.valueConverter, valueColumn, row, valueVector);
count++;
}
- InternalMap overflow = overflowMap(physicalChildren, row, context);
+ InternalMap overflow = overflowMap(physicalChildren, row,
context.physical);
if (overflow != null) {
InternalArray keys = overflow.keyArray();
InternalArray values = overflow.valueArray();
for (int i = 0; i < overflow.size(); i++) {
- BinaryString fieldName = context.nameById.get(keys.getInt(i));
+ BinaryString fieldName =
context.physical.nameById.get(keys.getInt(i));
if (fieldName != null) {
context.keyConverter.append(fieldName, keyVector);
- appendValue(context, values, i, valueVector);
+ appendValue(context.valueConverter, values, i,
valueVector);
count++;
}
}
@@ -227,50 +288,112 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
return count;
}
+ private static void appendSelectedKeyValue(
+ ColumnVector[] physicalChildren,
+ InternalArray fieldMapping,
+ @Nullable InternalMap overflow,
+ int row,
+ SelectedKeysContext context,
+ int ordinal,
+ WritableColumnVector valueVector) {
+ int fieldId = context.selectedKeyFieldIds[ordinal];
+ if (fieldId < 0) {
+ valueVector.appendNull();
+ return;
+ }
+
+ int[] candidateColumns = context.selectedKeyColumns[ordinal];
+ for (int i = 0; i < candidateColumns.length; i++) {
+ int column = candidateColumns[i];
+ if (mappedFieldId(fieldMapping, column) == fieldId) {
+ appendValue(
+ context.selectedValueConverters[ordinal],
+ physicalColumn(physicalChildren, column,
context.physical),
+ row,
+ valueVector);
+ return;
+ }
+ }
+
+ if (overflow != null) {
+ InternalArray keys = overflow.keyArray();
+ InternalArray values = overflow.valueArray();
+ for (int i = 0; i < overflow.size(); i++) {
+ if (!keys.isNullAt(i) && keys.getInt(i) == fieldId) {
+ appendValue(context.selectedValueConverters[ordinal],
values, i, valueVector);
+ return;
+ }
+ }
+ }
+
+ valueVector.appendNull();
+ }
+
private static void appendValue(
- SharedShreddingContext context,
+ RowToColumnConverter.ElementConverter converter,
ColumnVector source,
int row,
WritableColumnVector valueVector) {
if (source.isNullAt(row)) {
valueVector.appendNull();
} else {
- context.valueConverter.append(source, row, valueVector);
+ converter.append(source, row, valueVector);
}
}
private static void appendValue(
- SharedShreddingContext context,
+ RowToColumnConverter.ElementConverter converter,
InternalArray source,
int pos,
WritableColumnVector valueVector) {
if (source.isNullAt(pos)) {
valueVector.appendNull();
} else {
- context.valueConverter.append(source, pos, valueVector);
+ converter.append(source, pos, valueVector);
}
}
private static InternalArray fieldMapping(
- ColumnVector[] physicalChildren, int row, SharedShreddingContext
context) {
- InternalArray fieldMapping = ((ArrayColumnVector)
physicalChildren[0]).getArray(row);
+ ColumnVector[] physicalChildren, int row, SharedPhysicalContext
context) {
+ InternalArray fieldMapping =
+ ((ArrayColumnVector)
physicalChildren[FIELD_MAPPING_POSITION]).getArray(row);
checkArgument(
fieldMapping.size() == context.numColumns,
"Shared-shredding field mapping size %s does not match
metadata num columns %s.",
fieldMapping.size(),
context.numColumns);
+ for (int column = 0; column < fieldMapping.size(); column++) {
+ checkArgument(
+ !fieldMapping.isNullAt(column),
+ "Shared-shredding field mapping must not contain null at
column %s.",
+ column);
+ }
return fieldMapping;
}
private static BinaryString logicalFieldName(
- InternalArray fieldMapping, int column, SharedShreddingContext
context) {
- int fieldId = fieldMapping.isNullAt(column) ? -1 :
fieldMapping.getInt(column);
+ InternalArray fieldMapping, int column, SharedPhysicalContext
context) {
+ int fieldId = mappedFieldId(fieldMapping, column);
return fieldId < 0 ? null : context.nameById.get(fieldId);
}
+ private static int mappedFieldId(InternalArray fieldMapping, int column) {
+ return fieldMapping.getInt(column);
+ }
+
+ private static ColumnVector physicalColumn(
+ ColumnVector[] physicalChildren, int column, SharedPhysicalContext
context) {
+ int position = context.physicalColumnPositions[column];
+ checkArgument(
+ position >= 0,
+ "Shared-shredding physical column %s was not included in the
read schema.",
+ MapSharedShreddingDefine.physicalColumnName(column));
+ return physicalChildren[position];
+ }
+
private static InternalMap overflowMap(
- ColumnVector[] physicalChildren, int row, SharedShreddingContext
context) {
- if (context.overflowPosition >= physicalChildren.length) {
+ ColumnVector[] physicalChildren, int row, SharedPhysicalContext
context) {
+ if (context.overflowPosition < 0 || context.overflowPosition >=
physicalChildren.length) {
return null;
}
@@ -278,27 +401,156 @@ public class MapSharedShreddingReadPlan implements
ShreddingReadPlan {
return overflowVector.isNullAt(row) ? null : ((MapColumnVector)
overflowVector).getMap(row);
}
- private static class SharedShreddingContext {
+ private interface SharedShreddingContext {
+
+ ColumnVector materialize(RowColumnVector physicalVector, int rowCount);
+ }
+
+ private static class SharedPhysicalContext {
- private final MapType mapType;
private final Map<Integer, BinaryString> nameById;
- private final RowToColumnConverter.ElementConverter keyConverter;
- private final RowToColumnConverter.ElementConverter valueConverter;
private final int numColumns;
+ private final int[] physicalColumnPositions;
private final int overflowPosition;
- private SharedShreddingContext(MapSharedShreddingFieldMeta fieldMeta,
DataType fieldType) {
- this.mapType = (MapType) fieldType;
+ private SharedPhysicalContext(
+ MapSharedShreddingFieldMeta fieldMeta, RowType
physicalStructType) {
this.nameById = new LinkedHashMap<>();
for (Map.Entry<String, Integer> entry :
fieldMeta.nameToId().entrySet()) {
this.nameById.put(entry.getValue(),
BinaryString.fromString(entry.getKey()));
}
- this.keyConverter =
-
RowToColumnConverter.createElementConverter(this.mapType.getKeyType());
- this.valueConverter =
-
RowToColumnConverter.createElementConverter(this.mapType.getValueType());
this.numColumns = fieldMeta.numColumns();
- this.overflowPosition = fieldMeta.numColumns() + 1;
+ checkArgument(
+ physicalStructType.getFieldCount() > 0
+ && MapSharedShreddingDefine.FIELD_MAPPING.equals(
+
physicalStructType.getFieldNames().get(FIELD_MAPPING_POSITION)),
+ "Shared-shredding physical struct must start with %s.",
+ MapSharedShreddingDefine.FIELD_MAPPING);
+ this.physicalColumnPositions =
physicalColumnPositions(physicalStructType, numColumns);
+ this.overflowPosition = overflowPosition(physicalStructType);
+ }
+
+ private static int[] physicalColumnPositions(RowType
physicalStructType, int numColumns) {
+ int[] positions = new int[numColumns];
+ Arrays.fill(positions, -1);
+ int physicalColumnEnd = overflowPosition(physicalStructType);
+ if (physicalColumnEnd < 0) {
+ physicalColumnEnd = physicalStructType.getFieldCount();
+ }
+ for (int i = FIELD_MAPPING_POSITION + 1; i < physicalColumnEnd;
i++) {
+ String fieldName = physicalStructType.getFieldNames().get(i);
+ int column = physicalColumnIndex(fieldName);
+ checkArgument(
+ column >= 0,
+ "Unexpected shared-shredding physical field %s at
position %s.",
+ fieldName,
+ i);
+ checkArgument(
+ column < numColumns,
+ "Shared-shredding physical column %s exceeds metadata
num columns %s.",
+ fieldName,
+ numColumns);
+ positions[column] = i;
+ }
+ return positions;
+ }
+
+ private static int overflowPosition(RowType physicalStructType) {
+ int lastPosition = physicalStructType.getFieldCount() - 1;
+ if (lastPosition <= FIELD_MAPPING_POSITION) {
+ return -1;
+ }
+ return MapSharedShreddingDefine.OVERFLOW.equals(
+
physicalStructType.getFieldNames().get(lastPosition))
+ ? lastPosition
+ : -1;
+ }
+
+ private static int physicalColumnIndex(String fieldName) {
+ String prefix = "__col_";
+ if (!fieldName.startsWith(prefix)) {
+ return -1;
+ }
+ return Integer.parseInt(fieldName.substring(prefix.length()));
+ }
+ }
+
+ private static class FullMapContext implements SharedShreddingContext {
+
+ private final SharedPhysicalContext physical;
+ private final MapType mapType;
+ private final RowToColumnConverter.ElementConverter keyConverter;
+ private final RowToColumnConverter.ElementConverter valueConverter;
+
+ private FullMapContext(SharedPhysicalContext physical, MapType
mapType) {
+ this.physical = physical;
+ this.mapType = mapType;
+ this.keyConverter =
RowToColumnConverter.createElementConverter(mapType.getKeyType());
+ this.valueConverter =
+
RowToColumnConverter.createElementConverter(mapType.getValueType());
+ }
+
+ @Override
+ public ColumnVector materialize(RowColumnVector physicalVector, int
rowCount) {
+ return materializeLogicalMapVector(physicalVector, this, rowCount);
+ }
+ }
+
+ private static class SelectedKeysContext implements SharedShreddingContext
{
+
+ private final SharedPhysicalContext physical;
+ private final RowType selectedKeysType;
+ private final int[] selectedKeyFieldIds;
+ private final int[][] selectedKeyColumns;
+ private final boolean[] selectedKeyOverflow;
+ private final RowToColumnConverter.ElementConverter[]
selectedValueConverters;
+
+ private SelectedKeysContext(
+ SharedPhysicalContext physical,
+ MapSharedShreddingFieldMeta fieldMeta,
+ RowType selectedKeysType,
+ String selectedKeysMetadata) {
+ this.physical = physical;
+ this.selectedKeysType = selectedKeysType;
+ List<String> selectedKeys =
+
MapSelectedKeysMetadataUtils.selectedKeys(selectedKeysMetadata);
+ checkArgument(
+ selectedKeys.size() == selectedKeysType.getFieldCount(),
+ "Selected-key metadata size %s does not match selected ROW
field count %s.",
+ selectedKeys.size(),
+ selectedKeysType.getFieldCount());
+ this.selectedKeyFieldIds = new int[selectedKeys.size()];
+ this.selectedKeyColumns = new int[selectedKeys.size()][];
+ this.selectedKeyOverflow = new boolean[selectedKeys.size()];
+ this.selectedValueConverters =
+ new
RowToColumnConverter.ElementConverter[selectedKeys.size()];
+ for (int i = 0; i < selectedKeys.size(); i++) {
+ Integer fieldId =
fieldMeta.nameToId().get(selectedKeys.get(i));
+ this.selectedKeyFieldIds[i] = fieldId == null ? -1 : fieldId;
+ this.selectedKeyColumns[i] =
+ fieldId == null ? new int[0] :
candidateColumns(fieldMeta, fieldId);
+ this.selectedKeyOverflow[i] =
+ fieldId != null &&
fieldMeta.overflowFieldSet().contains(fieldId);
+ this.selectedValueConverters[i] =
+
RowToColumnConverter.createElementConverter(selectedKeysType.getTypeAt(i));
+ }
+ }
+
+ @Override
+ public ColumnVector materialize(RowColumnVector physicalVector, int
rowCount) {
+ return materializeSelectedKeysRowVector(physicalVector, this,
rowCount);
+ }
+
+ private static int[] candidateColumns(MapSharedShreddingFieldMeta
fieldMeta, int fieldId) {
+ List<Integer> columns = fieldMeta.fieldToColumns().get(fieldId);
+ if (columns == null || columns.isEmpty()) {
+ return new int[0];
+ }
+ int[] result = new int[columns.size()];
+ for (int i = 0; i < columns.size(); i++) {
+ result[i] = columns.get(i);
+ }
+ return result;
}
}
}
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 c2526b03bb..6fc2d13b5a 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
@@ -50,6 +50,8 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
/**
* Utility functions for the shared-shredding MAP storage layout.
*
@@ -115,18 +117,30 @@ public class MapSharedShreddingUtils {
for (DataField logicalReadField : logicalReadType.getFields()) {
MapSharedShreddingFieldMeta fieldMeta =
sharedShreddingFieldMetas.get(logicalReadField.name());
- if (fieldMeta == null || !(logicalReadField.type() instanceof
MapType)) {
+ if (fieldMeta == null) {
physicalReadFields.add(logicalReadField);
continue;
}
- MapType mapType = (MapType) logicalReadField.type();
- DataType physicalType =
- buildSpecificPhysicalStructType(
- mapType.getValueType(),
- fieldMeta.numColumns(),
- !fieldMeta.overflowFieldSet().isEmpty())
- .copy(logicalReadField.type().isNullable());
+ DataType valueType;
+ DataType physicalType;
+ if
(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(logicalReadField)) {
+ // recall partial key with shared shredding pushdown
+ valueType = selectedKeysValueType((RowType)
logicalReadField.type());
+ physicalType =
+ buildSpecificPhysicalStructType(
+ valueType,
+
selectedPhysicalColumnIds(logicalReadField, fieldMeta),
+
selectedKeysIncludeOverflow(logicalReadField, fieldMeta))
+ .copy(logicalReadField.type().isNullable());
+ } else {
+ // recall whole field without shared shredding pushdown
+ valueType = ((MapType) logicalReadField.type()).getValueType();
+ physicalType =
+ buildPhysicalStructType(valueType,
fieldMeta.numColumns())
+ .copy(logicalReadField.type().isNullable());
+ }
+
physicalReadFields.add(logicalReadField.newType(physicalType));
converted = true;
}
@@ -135,6 +149,19 @@ public class MapSharedShreddingUtils {
: logicalReadType;
}
+ private static DataType selectedKeysValueType(RowType selectedKeysType) {
+ checkArgument(
+ selectedKeysType.getFieldCount() > 0,
+ "Selected-key MAP read type must contain at least one field.");
+ DataType valueType = selectedKeysType.getTypeAt(0);
+ for (int i = 1; i < selectedKeysType.getFieldCount(); i++) {
+ checkArgument(
+
selectedKeysType.getTypeAt(i).equalsIgnoreNullable(valueType),
+ "Selected-key MAP fields must have the same value type.");
+ }
+ return valueType;
+ }
+
public static Map<String, Integer> buildColumnToNumColumns(
List<String> shreddingFieldNames, CoreOptions options) {
Map<String, Integer> fieldToNumColumns = new HashMap<>();
@@ -245,21 +272,21 @@ public class MapSharedShreddingUtils {
}
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();
+ return buildSpecificPhysicalStructType(valueType,
physicalColumnIds(numColumns), true);
}
- private static RowType buildSpecificPhysicalStructType(
- DataType valueType, int numColumns, boolean includeOverflow) {
+ public static RowType buildSpecificPhysicalStructType(
+ DataType valueType, Set<Integer> physicalColumnIds, boolean
includeOverflow) {
+ return innerBuildSpecificPhysicalStructType(
+ valueType, new ArrayList<>(new TreeSet<>(physicalColumnIds)),
includeOverflow);
+ }
+
+ private static RowType innerBuildSpecificPhysicalStructType(
+ DataType valueType, List<Integer> sortedColumns, boolean
includeOverflow) {
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);
+ for (Integer column : sortedColumns) {
+ builder.field(MapSharedShreddingDefine.physicalColumnName(column),
valueType);
}
if (includeOverflow) {
builder.field(MapSharedShreddingDefine.OVERFLOW, new MapType(new
IntType(), valueType));
@@ -267,6 +294,43 @@ public class MapSharedShreddingUtils {
return builder.build();
}
+ private static Set<Integer> physicalColumnIds(int numColumns) {
+ Set<Integer> physicalColumnIds = new TreeSet<>();
+ for (int i = 0; i < numColumns; i++) {
+ physicalColumnIds.add(i);
+ }
+ return physicalColumnIds;
+ }
+
+ private static Set<Integer> selectedPhysicalColumnIds(
+ DataField selectedKeysField, MapSharedShreddingFieldMeta
fieldMeta) {
+ Set<Integer> selectedColumns = new TreeSet<>();
+ for (String selectedKey :
+
MapSelectedKeysMetadataUtils.selectedKeys(selectedKeysField.description())) {
+ Integer fieldId = fieldMeta.nameToId().get(selectedKey);
+ if (fieldId == null) {
+ continue;
+ }
+ List<Integer> columns = fieldMeta.fieldToColumns().get(fieldId);
+ if (columns != null) {
+ selectedColumns.addAll(columns);
+ }
+ }
+ return selectedColumns;
+ }
+
+ private static boolean selectedKeysIncludeOverflow(
+ DataField selectedKeysField, MapSharedShreddingFieldMeta
fieldMeta) {
+ for (String selectedKey :
+
MapSelectedKeysMetadataUtils.selectedKeys(selectedKeysField.description())) {
+ Integer fieldId = fieldMeta.nameToId().get(selectedKey);
+ if (fieldId != null &&
fieldMeta.overflowFieldSet().contains(fieldId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private static Map<Integer, List<Integer>> sortedFieldColumns(
Map<Integer, List<Integer>> fieldToColumns) {
Map<Integer, List<Integer>> result = new TreeMap<>();
diff --git
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlanTest.java
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlanTest.java
index 943bf94dbf..6b781be417 100644
---
a/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlanTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/data/shredding/MapSharedShreddingReadPlanTest.java
@@ -20,10 +20,12 @@ package org.apache.paimon.data.shredding;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.InternalMap;
+import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.columnar.BytesColumnVector;
import org.apache.paimon.data.columnar.ColumnVector;
import org.apache.paimon.data.columnar.LongColumnVector;
import org.apache.paimon.data.columnar.MapColumnVector;
+import org.apache.paimon.data.columnar.RowColumnVector;
import org.apache.paimon.data.columnar.VectorizedColumnBatch;
import org.apache.paimon.data.columnar.heap.HeapArrayVector;
import org.apache.paimon.data.columnar.heap.HeapIntVector;
@@ -35,13 +37,16 @@ import org.apache.paimon.types.RowType;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Tests for {@link MapSharedShreddingReadPlan}. */
class MapSharedShreddingReadPlanTest {
@@ -88,6 +93,19 @@ class MapSharedShreddingReadPlanTest {
assertThat(restored.valueArray().getLong(0)).isEqualTo(30L);
}
+ @Test
+ void testFieldMappingRejectsNullElement() {
+ MapSharedShreddingFieldMeta fieldMeta =
+ new MapSharedShreddingFieldMeta(
+ nameToId("a", 0), Collections.emptyMap(), new
TreeSet<Integer>(), 2, 1);
+ HeapRowVector physicalMap =
+ rowVector(fieldMappingWithNull(0, null), longVector(10L),
longVector(null));
+
+ assertThatThrownBy(() -> readMap(fieldMeta, physicalMap))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("field mapping must not contain null");
+ }
+
@Test
void testAssembledMapVectorExposesKeyValueChildren() {
MapSharedShreddingFieldMeta fieldMeta =
@@ -114,6 +132,59 @@ class MapSharedShreddingReadPlanTest {
assertThat(mapVector.getMap(0).size()).isEqualTo(2);
}
+ @Test
+ void testReadSelectedKeysAsLogicalRowDirectly() {
+ MapSharedShreddingFieldMeta fieldMeta =
+ new MapSharedShreddingFieldMeta(
+ nameToId("key1", 0, "key2", 1, "cold", 2),
+ fieldToColumns(
+ 0, Collections.singletonList(0), 1,
Collections.singletonList(1)),
+ new TreeSet<Integer>(Collections.singletonList(1)),
+ 2,
+ 2);
+ HeapRowVector physicalMap =
+ rowVector(
+ fieldMapping(0, -1),
+ longVector(10L),
+ longVector(null),
+ overflowMap(1, 20L));
+
+ MapSharedShreddingReadPlan readPlan = selectedKeysReadPlan(fieldMeta);
+ RowColumnVector selectedKeysVector =
assembleSelectedKeysVector(readPlan, physicalMap);
+ InternalRow selectedKeys = selectedKeysVector.getRow(0);
+
+ assertThat(selectedKeys.getLong(0)).isEqualTo(10L);
+ assertThat(selectedKeys.getLong(1)).isEqualTo(20L);
+ assertThat(selectedKeys.isNullAt(2)).isTrue();
+ }
+
+ @Test
+ void testReadSelectedKeysFromPrunedPhysicalColumns() {
+ MapSharedShreddingFieldMeta fieldMeta =
+ new MapSharedShreddingFieldMeta(
+ nameToId("key1", 0, "key2", 1, "cold", 2),
+ fieldToColumns(
+ 0, Collections.singletonList(2), 2,
Collections.singletonList(0)),
+ new TreeSet<Integer>(Collections.singletonList(1)),
+ 4,
+ 2);
+
+ MapSharedShreddingReadPlan readPlan = selectedKeysReadPlan(fieldMeta);
+ RowType physicalMapType = (RowType)
readPlan.physicalRowType().getTypeAt(0);
+ assertThat(physicalMapType.getFieldNames())
+ .containsExactly("__field_mapping", "__col_2", "__overflow");
+
+ HeapRowVector physicalMap =
+ rowVector(fieldMapping(-1, -1, 0, -1), longVector(10L),
overflowMap(1, 20L));
+
+ RowColumnVector selectedKeysVector =
assembleSelectedKeysVector(readPlan, physicalMap);
+ InternalRow selectedKeys = selectedKeysVector.getRow(0);
+
+ assertThat(selectedKeys.getLong(0)).isEqualTo(10L);
+ assertThat(selectedKeys.getLong(1)).isEqualTo(20L);
+ assertThat(selectedKeys.isNullAt(2)).isTrue();
+ }
+
private static InternalMap readMap(
MapSharedShreddingFieldMeta fieldMeta, HeapRowVector physicalMap) {
return assembleMapVector(fieldMeta, physicalMap).getMap(0);
@@ -132,6 +203,24 @@ class MapSharedShreddingReadPlanTest {
return (MapColumnVector) logicalBatch.columns[0];
}
+ private static RowColumnVector assembleSelectedKeysVector(
+ MapSharedShreddingReadPlan readPlan, HeapRowVector physicalMap) {
+
assertThat(readPlan.physicalRowType().getTypeAt(0)).isInstanceOf(RowType.class);
+
+ VectorizedColumnBatch physicalBatch =
+ new VectorizedColumnBatch(new ColumnVector[] {physicalMap});
+ physicalBatch.setNumRows(1);
+ VectorizedColumnBatch logicalBatch =
readPlan.batchAssembler().assemble(physicalBatch);
+ return (RowColumnVector) logicalBatch.columns[0];
+ }
+
+ private static MapSharedShreddingReadPlan selectedKeysReadPlan(
+ MapSharedShreddingFieldMeta fieldMeta) {
+ Map<String, MapSharedShreddingFieldMeta> fieldMetas = new
LinkedHashMap<>();
+ fieldMetas.put("metrics", fieldMeta);
+ return new MapSharedShreddingReadPlan(selectedKeysLogicalType(),
fieldMetas);
+ }
+
private static RowType logicalType() {
return DataTypes.ROW(
DataTypes.FIELD(
@@ -140,6 +229,19 @@ class MapSharedShreddingReadPlanTest {
DataTypes.MAP(DataTypes.STRING().notNull(),
DataTypes.BIGINT())));
}
+ private static RowType selectedKeysLogicalType() {
+ RowType selectedKeysType =
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "0", DataTypes.BIGINT()),
+ DataTypes.FIELD(1, "1", DataTypes.BIGINT()),
+ DataTypes.FIELD(2, "2", DataTypes.BIGINT()));
+ return DataTypes.ROW(
+ MapSelectedKeysMetadataUtils.withSelectedKeys(
+ DataTypes.FIELD(0, "metrics", selectedKeysType),
+ selectedKeysType,
+ Arrays.asList("key1", "key2", "missing")));
+ }
+
private static HeapRowVector rowVector(ColumnVector... children) {
HeapRowVector vector = new HeapRowVector(1, children);
vector.appendRow();
@@ -156,6 +258,20 @@ class MapSharedShreddingReadPlanTest {
return vector;
}
+ private static HeapArrayVector fieldMappingWithNull(Integer... ids) {
+ HeapIntVector child = new HeapIntVector(ids.length);
+ for (Integer id : ids) {
+ if (id == null) {
+ child.appendNull();
+ } else {
+ child.appendInt(id);
+ }
+ }
+ HeapArrayVector vector = new HeapArrayVector(1, child);
+ vector.putOffsetLength(0, 0, ids.length);
+ return vector;
+ }
+
private static HeapLongVector longVector(Long value) {
HeapLongVector vector = new HeapLongVector(1);
if (value == null) {
@@ -182,4 +298,13 @@ class MapSharedShreddingReadPlanTest {
}
return result;
}
+
+ @SuppressWarnings("unchecked")
+ private static Map<Integer, List<Integer>> fieldToColumns(Object... pairs)
{
+ Map<Integer, List<Integer>> result = new TreeMap<>();
+ for (int i = 0; i < pairs.length; i += 2) {
+ result.put((Integer) pairs[i], (List<Integer>) pairs[i + 1]);
+ }
+ return result;
+ }
}
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 9c86109efa..dd628bc5a3 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
@@ -32,6 +32,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import java.util.TreeSet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -186,6 +187,33 @@ class MapSharedShreddingUtilsTest {
.isEqualTo(logical);
}
+ @Test
+ void testBuildSpecificPhysicalStructType() {
+ RowType physicalType =
+ MapSharedShreddingUtils.buildSpecificPhysicalStructType(
+ DataTypes.BIGINT().notNull(),
+ new TreeSet<Integer>(Arrays.asList(3, 1)),
+ true);
+
+ assertThat(physicalType.getFieldNames())
+ .containsExactly("__field_mapping", "__col_1", "__col_3",
"__overflow");
+
assertThat(physicalType.getFields()).extracting(DataField::id).containsExactly(0,
1, 2, 3);
+
assertThat(physicalType.getField("__col_1").type()).isEqualTo(DataTypes.BIGINT().notNull());
+ assertThat(physicalType.getField("__overflow").type())
+ .isEqualTo(DataTypes.MAP(DataTypes.INT(),
DataTypes.BIGINT().notNull()));
+ }
+
+ @Test
+ void testBuildSpecificPhysicalStructTypeWithoutOverflow() {
+ RowType physicalType =
+ MapSharedShreddingUtils.buildSpecificPhysicalStructType(
+ DataTypes.STRING(), new
TreeSet<Integer>(Arrays.asList(3)), false);
+
+
assertThat(physicalType.getFieldNames()).containsExactly("__field_mapping",
"__col_3");
+
assertThat(physicalType.getFields()).extracting(DataField::id).containsExactly(0,
1);
+
assertThat(physicalType.getField("__col_3").type()).isEqualTo(DataTypes.STRING());
+ }
+
@Test
void testMetadataRoundtrip() {
Map<String, Integer> nameToId = new TreeMap<>();
diff --git
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaManager.java
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaManager.java
index 4c238781b9..5eadd040c7 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaManager.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaManager.java
@@ -637,10 +637,56 @@ public class SchemaManager implements Serializable {
newSchema.primaryKeys(),
newSchema.options(),
newSchema.comment());
+ checkMapStorageLayoutUnchanged(oldTableSchema, newTableSchema);
SchemaValidation.validateTableSchema(newTableSchema);
return newTableSchema;
}
+ private static void checkMapStorageLayoutUnchanged(
+ TableSchema oldTableSchema, TableSchema newTableSchema) {
+ Map<Integer, CoreOptions.MapStorageLayout> oldLayouts =
+ mapStorageLayoutByFieldId(oldTableSchema);
+ Map<Integer, CoreOptions.MapStorageLayout> newLayouts =
+ mapStorageLayoutByFieldId(newTableSchema);
+ Map<Integer, String> oldNames = fieldNameById(oldTableSchema);
+ Map<Integer, String> newNames = fieldNameById(newTableSchema);
+
+ for (Map.Entry<Integer, CoreOptions.MapStorageLayout> oldLayout :
oldLayouts.entrySet()) {
+ Integer fieldId = oldLayout.getKey();
+ CoreOptions.MapStorageLayout newLayout = newLayouts.get(fieldId);
+ if (newLayout == null || oldLayout.getValue() == newLayout) {
+ continue;
+ }
+
+ throw new UnsupportedOperationException(
+ String.format(
+ "Cannot change map storage layout for field id %s
('%s' -> '%s') from '%s' to '%s'.",
+ fieldId,
+ oldNames.get(fieldId),
+ newNames.get(fieldId),
+ oldLayout.getValue(),
+ newLayout));
+ }
+ }
+
+ private static Map<Integer, CoreOptions.MapStorageLayout>
mapStorageLayoutByFieldId(
+ TableSchema schema) {
+ CoreOptions options = new CoreOptions(schema.options());
+ Map<Integer, CoreOptions.MapStorageLayout> layouts = new HashMap<>();
+ for (DataField field : schema.fields()) {
+ layouts.put(field.id(), options.mapStorageLayout(field.name()));
+ }
+ return layouts;
+ }
+
+ private static Map<Integer, String> fieldNameById(TableSchema schema) {
+ Map<Integer, String> names = new HashMap<>();
+ for (DataField field : schema.fields()) {
+ names.put(field.id(), field.name());
+ }
+ return names;
+ }
+
// gets the rootType at the defined depth
// ex: ARRAY<MAP<STRING, ARRAY<INT>>>
// if we want to update ARRAY<INT> -> ARRAY<BIGINT>
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/FormatReaderMapping.java
b/paimon-core/src/main/java/org/apache/paimon/utils/FormatReaderMapping.java
index 77395cd63a..e583f389d0 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/FormatReaderMapping.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/FormatReaderMapping.java
@@ -18,7 +18,10 @@
package org.apache.paimon.utils;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.CoreOptions.MapStorageLayout;
import org.apache.paimon.casting.CastFieldGetter;
+import org.apache.paimon.data.shredding.MapSelectedKeysMetadataUtils;
import org.apache.paimon.data.variant.VariantMetadataUtils;
import org.apache.paimon.format.FileFormatDiscover;
import org.apache.paimon.format.FormatReaderFactory;
@@ -44,10 +47,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Function;
import static
org.apache.paimon.predicate.PredicateBuilder.excludePredicateWithFields;
import static org.apache.paimon.table.SpecialFields.KEY_FIELD_ID_START;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
/** Class with index mapping and format reader. */
public class FormatReaderMapping {
@@ -205,7 +210,9 @@ public class FormatReaderMapping {
new ArrayList<>(fieldsExtractor.apply(dataSchema));
Map<String, Integer> systemFields =
findSystemFields(expectedFields);
- List<DataField> readDataFields =
readDataFields(allDataFieldsInFile, expectedFields);
+ Set<Integer> selectedKeysFieldIds =
selectedKeysFieldIds(tableSchema, expectedFields);
+ List<DataField> readDataFields =
+ readDataFields(allDataFieldsInFile, expectedFields,
selectedKeysFieldIds);
IndexCastMapping indexCastMapping =
SchemaEvolutionUtil.createIndexCastMapping(expectedFields,
readDataFields);
@@ -314,7 +321,9 @@ public class FormatReaderMapping {
}
private List<DataField> readDataFields(
- List<DataField> allDataFields, List<DataField> expectedFields)
{
+ List<DataField> allDataFields,
+ List<DataField> expectedFields,
+ Set<Integer> selectedKeysFieldIds) {
List<DataField> readDataFields = new ArrayList<>();
for (DataField dataField : allDataFields) {
expectedFields.stream()
@@ -322,6 +331,12 @@ public class FormatReaderMapping {
.findFirst()
.ifPresent(
field -> {
+ if
(selectedKeysFieldIds.contains(field.id())) {
+ checkSelectedKeysDataField(dataField);
+
readDataFields.add(selectedKeysDataField(field, dataField));
+ return;
+ }
+
DataType prunedType =
pruneDataType(field.type(),
dataField.type());
if (prunedType != null) {
@@ -332,6 +347,60 @@ public class FormatReaderMapping {
return readDataFields;
}
+ private DataField selectedKeysDataField(DataField expectedField,
DataField dataField) {
+ RowType selectedKeysType = (RowType) expectedField.type();
+ DataType dataValueType = ((MapType)
dataField.type()).getValueType();
+ List<DataField> selectedKeysDataFields = new ArrayList<>();
+ for (DataField selectedKeyField : selectedKeysType.getFields()) {
+ selectedKeysDataFields.add(
+ selectedKeyField.newType(
+
dataValueType.copy(selectedKeyField.type().isNullable())));
+ }
+ return dataField
+ .newType(selectedKeysType.copy(selectedKeysDataFields))
+ .newDescription(expectedField.description());
+ }
+
+ private void checkSelectedKeysDataField(DataField dataField) {
+ checkArgument(
+ dataField.type() instanceof MapType,
+ "Selected-key MAP field %s should be MAP type in data
schema.",
+ dataField.name());
+ }
+
+ private Set<Integer> selectedKeysFieldIds(
+ TableSchema tableSchema, List<DataField> expectedFields) {
+ CoreOptions options = CoreOptions.fromMap(tableSchema.options());
+ Map<Integer, DataField> tableFields = tableSchema.idToFieldMap();
+ Set<Integer> selectedKeysFieldIds = new HashSet<>();
+ for (DataField expectedField : expectedFields) {
+ DataField tableField = tableFields.get(expectedField.id());
+ if
(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(expectedField)
+ && tableField != null
+ && tableField.type() instanceof MapType) {
+ checkArgument(
+ options.mapStorageLayout(tableField.name())
+ == MapStorageLayout.SHARED_SHREDDING,
+ "Selected-key MAP pushdown only supports top-level
shared-shredding MAP field: %s.",
+ tableField.name());
+ validateSelectedKeyValueTypes(expectedField, tableField);
+ selectedKeysFieldIds.add(expectedField.id());
+ }
+ }
+ return selectedKeysFieldIds;
+ }
+
+ private void validateSelectedKeyValueTypes(DataField expectedField,
DataField tableField) {
+ RowType selectedKeysType = (RowType) expectedField.type();
+ DataType mapValueType = ((MapType)
tableField.type()).getValueType();
+ for (DataField selectedKeyField : selectedKeysType.getFields()) {
+ checkArgument(
+
selectedKeyField.type().equalsIgnoreNullable(mapValueType),
+ "Selected-key MAP pushdown does not support pruning
MAP value fields: %s.",
+ tableField.name());
+ }
+ }
+
@Nullable
private DataType pruneDataType(DataType readType, DataType dataType) {
switch (readType.getTypeRoot()) {
diff --git
a/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/TestMapOnlyAggFactory.java
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/TestMapOnlyAggFactory.java
new file mode 100644
index 0000000000..e6b4296a95
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/mergetree/compact/aggregate/TestMapOnlyAggFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mergetree.compact.aggregate;
+
+import org.apache.paimon.CoreOptions;
+import
org.apache.paimon.mergetree.compact.aggregate.factory.FieldAggregatorFactory;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.MapType;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** A custom MAP-only aggregator factory for testing {@link
FieldAggregatorFactory} discovery. */
+public class TestMapOnlyAggFactory implements FieldAggregatorFactory {
+
+ public static final String NAME = "my_merge_map";
+
+ @Override
+ public FieldAggregator create(DataType fieldType, CoreOptions options,
String field) {
+ checkArgument(
+ fieldType instanceof MapType,
+ "Data type for custom merge map column must be 'MAP' but was
'%s'",
+ fieldType);
+ return new FieldMergeMapAgg(identifier(), (MapType) fieldType);
+ }
+
+ @Override
+ public String identifier() {
+ return NAME;
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
index 1b3140a616..2b0f4f18bf 100644
--- a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
@@ -54,6 +54,7 @@ import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
import java.io.File;
import java.io.IOException;
@@ -170,6 +171,97 @@ public class SchemaManagerTest {
assertThat(latest.get().options()).containsEntry("new_k", "new_v");
}
+ @Test
+ public void testCannotChangeMapStorageLayoutForExistingField() throws
Exception {
+ retryArtificialException(() ->
manager.createTable(mapStorageLayoutSchema("default")));
+
+ assertThatThrownBy(
+ () ->
+ retryArtificialException(
+ () ->
+ manager.commitChanges(
+ SchemaChange.setOption(
+
"fields.metrics.map.storage-layout",
+
"shared-shredding"))))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining(
+ "Cannot change map storage layout for field id 1
('metrics' -> 'metrics') from 'default' to 'shared-shredding'.");
+ }
+
+ @Test
+ public void testCannotChangeMapStorageLayoutByRenameColumn() throws
Exception {
+ retryArtificialException(() ->
manager.createTable(mapStorageLayoutSchema(null)));
+
+ assertThatThrownBy(
+ () ->
+ retryArtificialException(
+ () ->
+ manager.commitChanges(
+ Arrays.asList(
+
SchemaChange.renameColumn(
+
"metrics",
+
"renamed_metrics"),
+
SchemaChange.setOption(
+
"fields.renamed_metrics.map.storage-layout",
+
"shared-shredding")))))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining(
+ "Cannot change map storage layout for field id 1
('metrics' -> 'renamed_metrics') from 'default' to 'shared-shredding'.");
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"plain", "sequential"})
+ public void testRenameColumnKeepsMapStorageLayoutOptions(String
placementPolicy)
+ throws Exception {
+ retryArtificialException(
+ () ->
+ manager.createTable(
+ mapStorageLayoutSchema("shared-shredding",
placementPolicy)));
+
+ retryArtificialException(
+ () ->
manager.commitChanges(SchemaChange.renameColumn("metrics", "renamed")));
+
+ Optional<TableSchema> latest = retryArtificialException(() ->
manager.latest());
+ assertThat(latest.isPresent()).isTrue();
+ assertThat(latest.get().options())
+ .doesNotContainKeys(
+ "fields.metrics.map.storage-layout",
+ "fields.metrics.map.shared-shredding.max-columns",
+
"fields.metrics.map.shared-shredding.column-placement-policy")
+ .containsEntry("fields.renamed.map.storage-layout",
"shared-shredding")
+
.containsEntry("fields.renamed.map.shared-shredding.max-columns", "2")
+ .containsEntry(
+
"fields.renamed.map.shared-shredding.column-placement-policy",
+ placementPolicy);
+ }
+
+ private Schema mapStorageLayoutSchema(String layout) {
+ return mapStorageLayoutSchema(layout, null);
+ }
+
+ private Schema mapStorageLayoutSchema(String layout, String
placementPolicy) {
+ Map<String, String> options = new HashMap<>();
+ if (layout != null) {
+ options.put("fields.metrics.map.storage-layout", layout);
+ options.put("fields.metrics.map.shared-shredding.max-columns",
"2");
+ }
+ if (placementPolicy != null) {
+ options.put(
+
"fields.metrics.map.shared-shredding.column-placement-policy", placementPolicy);
+ }
+ return new Schema(
+ Arrays.asList(
+ new DataField(0, "id", DataTypes.INT()),
+ new DataField(
+ 1,
+ "metrics",
+ DataTypes.MAP(DataTypes.STRING().notNull(),
DataTypes.BIGINT()))),
+ Collections.emptyList(),
+ Collections.emptyList(),
+ options,
+ "");
+ }
+
@Test
public void testRejectRenamePrimaryKeyVectorIndexColumn() throws Exception
{
Map<String, String> options = new HashMap<>();
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
index fe06144044..c300bb4a70 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/MapSharedShreddingTableTest.java
@@ -31,6 +31,7 @@ import org.apache.paimon.data.InternalArray;
import org.apache.paimon.data.InternalMap;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.shredding.MapSelectedKeysMetadataUtils;
import org.apache.paimon.data.shredding.MapSharedShreddingFieldMeta;
import org.apache.paimon.data.shredding.MapSharedShreddingUtils;
import org.apache.paimon.format.FileFormat;
@@ -75,6 +76,7 @@ import java.util.stream.Stream;
import static
org.apache.paimon.deletionvectors.DeletionVectorsIndexFile.DELETION_VECTORS_INDEX;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** Table-level tests for MAP shared-shredding. */
public class MapSharedShreddingTableTest extends TableTestBase {
@@ -159,6 +161,130 @@ public class MapSharedShreddingTableTest extends
TableTestBase {
.containsEntry(4, javaMapOf("a", 70L, "b", 80L, "c", 90L, "d",
100L));
}
+ @ParameterizedTest
+ @ValueSource(strings = {"orc", "parquet"})
+ public void testReadSelectedKeysAsRow(String format) throws Exception {
+ Table table = createTable(format, 1, "metrics");
+
+ write(
+ table,
+ GenericRow.of(1, mapOf("key1", 10L, "key2", 20L)),
+ GenericRow.of(2, mapOf("key2", 30L, "cold", 40L)),
+ GenericRow.of(3, null));
+
+ Map<Integer, List<Long>> actual = new LinkedHashMap<>();
+ ReadBuilder readBuilder =
table.newReadBuilder().withReadType(selectedKeysReadType());
+ RecordReader<InternalRow> reader =
+
readBuilder.newRead().createReader(readBuilder.newScan().plan());
+ reader.forEachRemaining(
+ row -> {
+ if (row.isNullAt(1)) {
+ actual.put(row.getInt(0), null);
+ } else {
+ InternalRow selectedKeys = row.getRow(1, 3);
+ actual.put(
+ row.getInt(0),
+ Arrays.asList(
+ selectedKeys.isNullAt(0) ? null :
selectedKeys.getLong(0),
+ selectedKeys.isNullAt(1) ? null :
selectedKeys.getLong(1),
+ selectedKeys.isNullAt(2) ? null :
selectedKeys.getLong(2)));
+ }
+ });
+
+ assertThat(actual)
+ .containsEntry(1, Arrays.asList(10L, 20L, null))
+ .containsEntry(2, Arrays.asList(null, 30L, null))
+ .containsEntry(3, null);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"orc", "parquet"})
+ public void testReadSelectedKeysAfterMapValueTypeEvolution(String format)
throws Exception {
+ catalog.createTable(
+ identifier(format),
+ Schema.newBuilder()
+ .column("id", DataTypes.INT())
+ .column(
+ "metrics",
+ DataTypes.MAP(DataTypes.STRING().notNull(),
DataTypes.INT()))
+ .option("bucket", "-1")
+ .option("file.format", format)
+ .option(CoreOptions.WRITE_ONLY.key(), "true")
+ .option("fields.metrics.map.storage-layout",
"shared-shredding")
+
.option("fields.metrics.map.shared-shredding.max-columns", "1")
+ .build(),
+ true);
+ Table table = catalog.getTable(identifier(format));
+ Map<BinaryString, Integer> values = new LinkedHashMap<>();
+ values.put(BinaryString.fromString("key1"), 10);
+ write(table, GenericRow.of(1, new GenericMap(values)));
+
+ catalog.alterTable(
+ identifier(format),
+ Collections.singletonList(
+ SchemaChange.updateColumnType(
+ new String[] {"metrics", "value"},
DataTypes.BIGINT(), false)),
+ false);
+ table = catalog.getTable(identifier(format));
+
+ ReadBuilder readBuilder =
table.newReadBuilder().withReadType(selectedKeysReadType());
+ List<List<Long>> actual = new ArrayList<>();
+ readBuilder
+ .newRead()
+ .createReader(readBuilder.newScan().plan())
+ .forEachRemaining(
+ row -> {
+ InternalRow selectedKeys = row.getRow(1, 3);
+ actual.add(
+ Arrays.asList(
+ selectedKeys.getLong(0),
+ selectedKeys.isNullAt(1)
+ ? null
+ : selectedKeys.getLong(1),
+ selectedKeys.isNullAt(2)
+ ? null
+ :
selectedKeys.getLong(2)));
+ });
+ assertThat(actual).containsExactly(Arrays.asList(10L, null, null));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"orc", "parquet"})
+ public void testReadSelectedKeysAfterRenameColumn(String format) throws
Exception {
+ Table table = createTable(format, 1, "metrics");
+ write(
+ table,
+ GenericRow.of(1, mapOf("key1", 10L, "key2", 20L)),
+ GenericRow.of(2, mapOf("key2", 30L)));
+
+ catalog.alterTable(
+ identifier(format),
+ Collections.singletonList(SchemaChange.renameColumn("metrics",
"renamed_metrics")),
+ false);
+ table = catalog.getTable(identifier(format));
+
+ ReadBuilder readBuilder =
+
table.newReadBuilder().withReadType(selectedKeysReadType("renamed_metrics"));
+ Map<Integer, List<Long>> actual = new LinkedHashMap<>();
+ try (RecordReader<InternalRow> reader =
+
readBuilder.newRead().createReader(readBuilder.newScan().plan())) {
+ reader.forEachRemaining(
+ row -> {
+ InternalRow selectedKeys = row.getRow(1, 3);
+ actual.put(
+ row.getInt(0),
+ Arrays.asList(
+ selectedKeys.isNullAt(0) ? null :
selectedKeys.getLong(0),
+ selectedKeys.isNullAt(1) ? null :
selectedKeys.getLong(1),
+ selectedKeys.isNullAt(2) ? null :
selectedKeys.getLong(2)));
+ });
+ }
+
+ assertThat(actual)
+ .containsEntry(1, Arrays.asList(10L, 20L, null))
+ .containsEntry(2, Arrays.asList(null, 30L, null));
+ }
+
@ParameterizedTest
@ValueSource(strings = {"orc", "parquet"})
public void testAppendOnlyTableReadWriteWithTwoMapFields(String format)
throws Exception {
@@ -731,53 +857,29 @@ public class MapSharedShreddingTableTest extends
TableTestBase {
@ParameterizedTest
@ValueSource(strings = {"orc", "parquet"})
- public void testSwitchMapLayoutAndInferColumns(String format) throws
Exception {
- Table table =
- createTableWithBucket(
- format,
- 4,
- "1",
- Arrays.asList("metrics", "labels"),
- Arrays.asList("labels"));
-
- write(table, GenericRow.of(1, mapOf("a", 11L, "b", 12L), mapOf("x",
21L)));
-
- catalog.alterTable(
- identifier(format),
- Arrays.asList(
- SchemaChange.setOption(
- "fields.metrics.map.storage-layout",
"shared-shredding"),
- SchemaChange.setOption(
-
"fields.metrics.map.shared-shredding.max-columns", "3"),
-
SchemaChange.setOption("fields.labels.map.storage-layout", "default")),
- false);
- table = catalog.getTable(identifier(format));
-
- write(table, GenericRow.of(2, mapOf("c", 31L), mapOf("y", 41L, "z",
42L)));
-
- FileStoreTable fileStoreTable = (FileStoreTable) table;
- List<DataFileWithSplit> files = currentDataFiles(fileStoreTable);
- files.sort(Comparator.comparingLong(file ->
file.dataFile.minSequenceNumber()));
- assertThat(files).hasSize(2);
-
- MapSharedShreddingFieldMeta metricsMeta =
- readSharedShreddingFieldMeta(fileStoreTable, files.get(1),
"metrics");
- assertThat(metricsMeta.numColumns()).isEqualTo(3);
- assertThat(metricsMeta.maxRowWidth()).isEqualTo(1);
-
- Map<Integer, List<Map<String, Long>>> actual = new LinkedHashMap<>();
- for (InternalRow row : read(table)) {
- actual.put(
- row.getInt(0),
- Arrays.asList(
- row.isNullAt(1) ? null : toJavaMap(row.getMap(1)),
- row.isNullAt(2) ? null :
toJavaMap(row.getMap(2))));
- }
+ public void
testCannotSwitchMapLayoutAndUseMaxColumnsWithoutMetadata(String format)
+ throws Exception {
+ createTableWithBucket(
+ format, 4, "1", Arrays.asList("metrics", "labels"),
Arrays.asList("labels"));
- assertThat(actual)
- .containsEntry(1, Arrays.asList(javaMapOf("a", 11L, "b", 12L),
javaMapOf("x", 21L)))
- .containsEntry(
- 2, Arrays.asList(javaMapOf("c", 31L), javaMapOf("y",
41L, "z", 42L)));
+ assertThatThrownBy(
+ () ->
+ catalog.alterTable(
+ identifier(format),
+ Arrays.asList(
+ SchemaChange.setOption(
+
"fields.metrics.map.storage-layout",
+ "shared-shredding"),
+ SchemaChange.setOption(
+
"fields.metrics.map.shared-shredding.max-columns",
+ "3"),
+ SchemaChange.setOption(
+
"fields.labels.map.storage-layout",
+ "default")),
+ false))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining(
+ "Cannot change map storage layout for field id 1
('metrics' -> 'metrics') from 'default' to 'shared-shredding'.");
}
@ParameterizedTest
@@ -1372,6 +1474,24 @@ public class MapSharedShreddingTableTest extends
TableTestBase {
return schemaWithBucket(format, maxColumns, "-1",
sharedShreddingFields);
}
+ private RowType selectedKeysReadType() {
+ return selectedKeysReadType("metrics");
+ }
+
+ private RowType selectedKeysReadType(String fieldName) {
+ RowType selectedKeysType =
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "0", DataTypes.BIGINT()),
+ DataTypes.FIELD(1, "1", DataTypes.BIGINT()),
+ DataTypes.FIELD(2, "2", DataTypes.BIGINT()));
+ return DataTypes.ROW(
+ DataTypes.FIELD(0, "id", DataTypes.INT()),
+ MapSelectedKeysMetadataUtils.withSelectedKeys(
+ DataTypes.FIELD(1, fieldName, selectedKeysType),
+ selectedKeysType,
+ Arrays.asList("key1", "key2", "missing")));
+ }
+
private Schema schemaWithBucket(
String format, int maxColumns, String bucket, String...
sharedShreddingFields) {
return schemaWithBucket(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/utils/FormatReaderMappingTest.java
b/paimon-core/src/test/java/org/apache/paimon/utils/FormatReaderMappingTest.java
index 7b2ad9898d..88f8d94366 100644
---
a/paimon-core/src/test/java/org/apache/paimon/utils/FormatReaderMappingTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/utils/FormatReaderMappingTest.java
@@ -18,8 +18,12 @@
package org.apache.paimon.utils;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.shredding.MapSelectedKeysMetadataUtils;
import org.apache.paimon.schema.IndexCastMapping;
import org.apache.paimon.schema.SchemaEvolutionUtil;
+import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.table.SpecialFields;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
@@ -28,9 +32,14 @@ import org.apache.paimon.types.RowType;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.lang.reflect.Method;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/** Test for {@link FormatReaderMapping.Builder}. */
public class FormatReaderMappingTest {
@@ -147,4 +156,212 @@ public class FormatReaderMappingTest {
Assertions.assertThat(trimmed.get(2).id()).isEqualTo(3);
Assertions.assertThat(trimmed.size()).isEqualTo(3);
}
+
+ @Test
+ public void testMapSelectedKeysKeepsSelectedRowTypeForFormatReader()
throws Exception {
+ RowType dataValueType =
+ DataTypes.ROW(
+ DataTypes.FIELD(10, "a", DataTypes.INT()),
+ DataTypes.FIELD(11, "b", DataTypes.INT()),
+ DataTypes.FIELD(12, "c", DataTypes.INT()));
+ RowType selectedKeysType =
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "0", dataValueType),
+ DataTypes.FIELD(1, "1", dataValueType));
+
+ DataField dataField =
+ DataTypes.FIELD(2, "attrs", DataTypes.MAP(DataTypes.STRING(),
dataValueType));
+ DataField expectedField =
+ MapSelectedKeysMetadataUtils.withSelectedKeys(
+ DataTypes.FIELD(2, "attrs", selectedKeysType),
+ selectedKeysType,
+ Arrays.asList("key1", "key2"));
+
+ List<DataField> dataFields = Collections.singletonList(dataField);
+ List<DataField> expectedFields =
Collections.singletonList(expectedField);
+
+ DataField readField = invokeDataFields(dataFields,
expectedFields).get(0);
+
+ Assertions.assertThat(readField.type()).isEqualTo(selectedKeysType);
+
Assertions.assertThat(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(readField))
+ .isTrue();
+ }
+
+ @Test
+ public void testMapSelectedKeysUsesDataValueTypeForSchemaEvolution()
throws Exception {
+ DataField dataField =
+ DataTypes.FIELD(2, "attrs", DataTypes.MAP(DataTypes.STRING(),
DataTypes.INT()));
+ DataField expectedField = selectedKeysField(2, "attrs",
DataTypes.BIGINT());
+
+ DataField readField =
+ invokeDataFields(
+ Collections.singletonList(dataField),
+ Collections.singletonList(expectedField))
+ .get(0);
+ RowType readType = (RowType) readField.type();
+
Assertions.assertThat(readType.getTypeAt(0)).isEqualTo(DataTypes.INT());
+
Assertions.assertThat(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(readField))
+ .isTrue();
+
+ IndexCastMapping mapping =
+ SchemaEvolutionUtil.createIndexCastMapping(
+ Collections.singletonList(expectedField),
+ Collections.singletonList(readField));
+ Assertions.assertThat(mapping.getCastMapping()).isNotNull();
+ InternalRow selectedKeys =
+
mapping.getCastMapping()[0].getFieldOrNull(GenericRow.of(GenericRow.of(10)));
+ Assertions.assertThat(selectedKeys.getLong(0)).isEqualTo(10L);
+ }
+
+ @Test
+ public void testMapSelectedKeysUsesDataFieldNameAfterRename() throws
Exception {
+ DataField dataField =
+ DataTypes.FIELD(
+ 2, "old_attrs", DataTypes.MAP(DataTypes.STRING(),
DataTypes.BIGINT()));
+ DataField expectedField = selectedKeysField(2, "new_attrs",
DataTypes.BIGINT());
+
+ DataField readField =
+ invokeDataFields(
+ Collections.singletonList(dataField),
+ Collections.singletonList(expectedField))
+ .get(0);
+
+ Assertions.assertThat(readField.name()).isEqualTo("old_attrs");
+
Assertions.assertThat(readField.description()).isEqualTo(expectedField.description());
+
Assertions.assertThat(MapSelectedKeysMetadataUtils.isMapSelectedKeysField(readField))
+ .isTrue();
+ }
+
+ @Test
+ public void testRejectMapSelectedKeysValuePartialProjection() {
+ RowType tableValueType =
+ DataTypes.ROW(
+ DataTypes.FIELD(10, "a", DataTypes.INT()),
+ DataTypes.FIELD(11, "b", DataTypes.INT()));
+ RowType selectedValueType = DataTypes.ROW(DataTypes.FIELD(10, "a",
DataTypes.INT()));
+ DataField tableField =
+ DataTypes.FIELD(2, "attrs", DataTypes.MAP(DataTypes.STRING(),
tableValueType));
+ DataField expectedField = selectedKeysField(2, "attrs",
selectedValueType);
+
+ Map<String, String> options = new HashMap<>();
+ options.put("fields.attrs.map.storage-layout", "shared-shredding");
+
+ Assertions.assertThatThrownBy(
+ () ->
+ invokeSelectedKeysFieldIds(
+
tableSchema(Collections.singletonList(tableField), options),
+
Collections.singletonList(expectedField)))
+ .hasRootCauseMessage(
+ "Selected-key MAP pushdown does not support pruning
MAP value fields: attrs.");
+ }
+
+ @Test
+ public void testMapSelectedKeysOnlySupportsTopLevelSharedShreddingMap()
throws Exception {
+ DataField tableField =
+ DataTypes.FIELD(2, "attrs", DataTypes.MAP(DataTypes.STRING(),
DataTypes.BIGINT()));
+ DataField expectedField = selectedKeysField(2, "attrs",
DataTypes.BIGINT());
+
+ Map<String, String> options = new HashMap<>();
+ options.put("fields.attrs.map.storage-layout", "shared-shredding");
+ Assertions.assertThat(
+ invokeSelectedKeysFieldIds(
+
tableSchema(Collections.singletonList(tableField), options),
+ Collections.singletonList(expectedField)))
+ .containsExactly(2);
+
+ Assertions.assertThatThrownBy(
+ () ->
+ invokeSelectedKeysFieldIds(
+ tableSchema(
+
Collections.singletonList(tableField),
+ Collections.emptyMap()),
+
Collections.singletonList(expectedField)))
+ .hasRootCauseMessage(
+ "Selected-key MAP pushdown only supports top-level
shared-shredding MAP field: attrs.");
+ }
+
+ @Test
+ public void testNormalRowWithSelectedKeysMetadataComment() throws
Exception {
+ RowType rowType = DataTypes.ROW(DataTypes.FIELD(3, "value",
DataTypes.BIGINT()));
+ DataField rowField =
+ DataTypes.FIELD(2, "payload", rowType)
+
.newDescription(MapSelectedKeysMetadataUtils.METADATA_KEY + "key1");
+ TableSchema tableSchema =
+ tableSchema(Collections.singletonList(rowField),
Collections.emptyMap());
+
+ Set<Integer> selectedKeysFieldIds =
+ invokeSelectedKeysFieldIds(tableSchema,
Collections.singletonList(rowField));
+
+ Assertions.assertThat(selectedKeysFieldIds).isEmpty();
+ Assertions.assertThat(
+ invokeDataFields(
+ Collections.singletonList(rowField),
+ Collections.singletonList(rowField),
+ selectedKeysFieldIds))
+ .containsExactly(rowField);
+ }
+
+ @Test
+ public void testRejectSelectedKeysDataFieldWithNonMapType() {
+ DataField dataField = DataTypes.FIELD(2, "attrs", DataTypes.BIGINT());
+ DataField expectedField = selectedKeysField(2, "attrs",
DataTypes.BIGINT());
+
+ Assertions.assertThatThrownBy(
+ () ->
+ invokeDataFields(
+ Collections.singletonList(dataField),
+
Collections.singletonList(expectedField)))
+ .hasRootCauseMessage(
+ "Selected-key MAP field attrs should be MAP type in
data schema.");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<DataField> invokeDataFields(
+ List<DataField> allDataFields, List<DataField> expectedFields)
throws Exception {
+ return invokeDataFields(allDataFields, expectedFields,
Collections.singleton(2));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<DataField> invokeDataFields(
+ List<DataField> allDataFields,
+ List<DataField> expectedFields,
+ Set<Integer> selectedKeysFieldIds)
+ throws Exception {
+ FormatReaderMapping.Builder builder =
+ new FormatReaderMapping.Builder(
+ null, Collections.emptyList(), null, null, null, null);
+ Method method =
+ FormatReaderMapping.Builder.class.getDeclaredMethod(
+ "readDataFields", List.class, List.class, Set.class);
+ method.setAccessible(true);
+ return (List<DataField>)
+ method.invoke(builder, allDataFields, expectedFields,
selectedKeysFieldIds);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Set<Integer> invokeSelectedKeysFieldIds(
+ TableSchema tableSchema, List<DataField> expectedFields) throws
Exception {
+ FormatReaderMapping.Builder builder =
+ new FormatReaderMapping.Builder(
+ null, Collections.emptyList(), null, null, null, null);
+ Method method =
+ FormatReaderMapping.Builder.class.getDeclaredMethod(
+ "selectedKeysFieldIds", TableSchema.class, List.class);
+ method.setAccessible(true);
+ return (Set<Integer>) method.invoke(builder, tableSchema,
expectedFields);
+ }
+
+ private static DataField selectedKeysField(
+ int id, String name, org.apache.paimon.types.DataType valueType) {
+ RowType selectedKeysType = DataTypes.ROW(DataTypes.FIELD(0, "0",
valueType));
+ return MapSelectedKeysMetadataUtils.withSelectedKeys(
+ DataTypes.FIELD(id, name, selectedKeysType),
+ selectedKeysType,
+ Collections.singletonList("key1"));
+ }
+
+ private static TableSchema tableSchema(List<DataField> fields, Map<String,
String> options) {
+ return new TableSchema(
+ 1, fields, 100, Collections.emptyList(),
Collections.emptyList(), options, "");
+ }
}
diff --git
a/paimon-core/src/test/resources/META-INF/services/org.apache.paimon.factories.Factory
b/paimon-core/src/test/resources/META-INF/services/org.apache.paimon.factories.Factory
index b44cf4490a..76ffd7a52f 100644
---
a/paimon-core/src/test/resources/META-INF/services/org.apache.paimon.factories.Factory
+++
b/paimon-core/src/test/resources/META-INF/services/org.apache.paimon.factories.Factory
@@ -14,4 +14,5 @@
# limitations under the License.
org.apache.paimon.mergetree.compact.aggregate.TestCustomAggFactory
-org.apache.paimon.rest.auth.CustomTestDLFTokenLoaderFactory
\ No newline at end of file
+org.apache.paimon.mergetree.compact.aggregate.TestMapOnlyAggFactory
+org.apache.paimon.rest.auth.CustomTestDLFTokenLoaderFactory
diff --git
a/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
b/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
new file mode 100644
index 0000000000..1bdf43ce20
--- /dev/null
+++
b/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETest.scala
@@ -0,0 +1,21 @@
+/*
+ * 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.spark.sql
+
+class MapSelectedKeysSharedShreddingE2ETest extends
MapSelectedKeysSharedShreddingE2ETestBase {}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownMapSelectedKeys.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownMapSelectedKeys.scala
index 16db25531f..5ec0e9e281 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownMapSelectedKeys.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/optimizer/PushDownMapSelectedKeys.scala
@@ -187,18 +187,25 @@ abstract class PushDownMapSelectedKeysBase extends
Rule[LogicalPlan] {
return false
}
+ val options = CoreOptions.fromMap(scan.table.options())
access.mapType match {
case MapType(StringType, _, _) =>
fieldType(scan.table.rowType(), access.fieldName) match {
case Some(mapType: org.apache.paimon.types.MapType) if
isStringKeyMap(mapType) =>
-
CoreOptions.fromMap(scan.table.options()).mapStorageLayout(access.fieldName) ==
- MapStorageLayout.SHARED_SHREDDING
+ options.mapStorageLayout(access.fieldName) ==
MapStorageLayout.SHARED_SHREDDING &&
+ !hasConfiguredAggregator(options, access.fieldName)
case _ => false
}
case _ => false
}
}
+ private def hasConfiguredAggregator(options: CoreOptions, fieldName:
String): Boolean = {
+ Option(options.fieldAggFunc(fieldName))
+ .orElse(Option(options.fieldsDefaultFunc()))
+ .isDefined
+ }
+
private def canEncodeKey(key: String): Boolean = {
!key.contains(MapSelectedKeysMetadataUtils.KEY_DELIMITER) &&
!key.startsWith(MapSelectedKeysMetadataUtils.METADATA_KEY)
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
index 24b32671c9..53a10b0287 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/execution/PaimonStrategy.scala
@@ -27,6 +27,7 @@ import org.apache.paimon.predicate.{Predicate,
PredicateBuilder}
import org.apache.paimon.spark.{PaimonRecordReaderIterator, PaimonScan,
PostponeMergeInputScan, PostponeMergeOnRead, SparkCatalog, SparkGenericCatalog,
SparkTable, SparkUtils}
import org.apache.paimon.spark.catalog.{SparkBaseCatalog, SupportView}
import org.apache.paimon.spark.catalyst.analysis.ResolvedPaimonView
+import org.apache.paimon.spark.catalyst.optimizer.PushDownMapSelectedKeys
import
org.apache.paimon.spark.catalyst.optimizer.RepartitionLateralVectorSearchInput
import
org.apache.paimon.spark.catalyst.plans.logical.{CopyIntoLocationCommand,
CopyIntoLocationSource, CopyIntoTableCommand, CreateOrReplaceTagCommand,
CreatePaimonView, DeleteTagCommand, DropPaimonView, LateralVectorSearch,
PaimonCallCommand, PaimonDropPartitions, PaimonTableValuedFunctions,
RenameTagCommand, ResolvedIdentifier, ShowPaimonViews, ShowTagsCommand,
TruncatePaimonTableWithFilter}
import org.apache.paimon.spark.data.SparkInternalRow
@@ -68,7 +69,19 @@ case class PaimonStrategy(spark: SparkSession)
import DataSourceV2Implicits._
protected lazy val catalogManager = spark.sessionState.catalogManager
- override def apply(plan: LogicalPlan): Seq[SparkPlan] = plan match {
+ override def apply(plan: LogicalPlan): Seq[SparkPlan] = {
+ // Spark creates DataSourceV2ScanRelation after injected optimizer rules
have run. Apply this
+ // rewrite during physical planning, when the scan relation is available,
and let the regular
+ // Spark strategies plan the rewritten logical subtree.
+ val rewritten = PushDownMapSelectedKeys(plan)
+ if (!rewritten.fastEquals(plan)) {
+ planLater(rewritten) :: Nil
+ } else {
+ applyWithoutMapSelectedKeysPushDown(plan)
+ }
+ }
+
+ private def applyWithoutMapSelectedKeysPushDown(plan: LogicalPlan):
Seq[SparkPlan] = plan match {
case PhysicalOperation(projects, filters, relation:
DataSourceV2ScanRelation) =>
relation.scan match {
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
index 95c2657036..e3701a468a 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
@@ -101,8 +101,6 @@ class PaimonSparkSessionExtensions extends
(SparkSessionExtensions => Unit) {
// optimization rules
extensions.injectOptimizerRule(spark => ReplacePaimonFunctions(spark))
extensions.injectOptimizerRule(spark =>
OptimizeMetadataOnlyDeleteFromPaimonTable(spark))
- // TODO: Enable MAP selected-key pushdown after core reader supports
- // __PAIMON_MAP_SELECTED_KEYS read type.
extensions.injectOptimizerRule(_ => MergePaimonScalarSubqueries)
extensions.injectOptimizerRule(_ => RepartitionLateralVectorSearchInput)
extensions.injectOptimizerRule(_ => PushDownLateralVectorSearchFilter)
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETestBase.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETestBase.scala
new file mode 100644
index 0000000000..f2351ac965
--- /dev/null
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/MapSelectedKeysSharedShreddingE2ETestBase.scala
@@ -0,0 +1,215 @@
+/*
+ * 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.spark.sql
+
+import org.apache.paimon.spark.{PaimonScan, PaimonSparkTestBase}
+
+import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
+
+abstract class MapSelectedKeysSharedShreddingE2ETestBase extends
PaimonSparkTestBase {
+
+ Seq("parquet", "orc").foreach {
+ format =>
+ test(s"read selected shared-shredding map keys directly from $format") {
+ withTable("T") {
+ sql(s"""
+ |CREATE TABLE T (id INT, attrs MAP<STRING, BIGINT>)
+ |TBLPROPERTIES (
+ | 'bucket' = '-1',
+ | 'file.format' = '$format',
+ | 'fields.attrs.map.storage-layout' = 'shared-shredding',
+ | 'fields.attrs.map.shared-shredding.max-columns' = '1'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key1', CAST(10 AS BIGINT), 'key2', CAST(20 AS
BIGINT))),
+ | (2, map('key2', CAST(30 AS BIGINT), 'cold', CAST(40 AS
BIGINT)))
+ |""".stripMargin)
+
+ val query =
+ sql("SELECT id, attrs['key1'], attrs['key2'], attrs['missing']
FROM T ORDER BY id")
+ val sparkPlan = query.queryExecution.sparkPlan
+ val pushedMapSelectedKeys = sparkPlan.collectFirst {
+ case scan: BatchScanExec if scan.scan.isInstanceOf[PaimonScan] =>
+ scan.scan.asInstanceOf[PaimonScan].pushedMapSelectedKeys
+ }
+ val expectedPushedMapSelectedKeys = Map("attrs" -> Seq("key1",
"key2", "missing"))
+ assert(
+ pushedMapSelectedKeys.contains(expectedPushedMapSelectedKeys),
+ s"""Expected selected MAP keys to be pushed down.
+ |Physical plan:
+ |$sparkPlan""".stripMargin
+ )
+
+ checkAnswer(query, Row(1, 10L, 20L, null) :: Row(2, null, 30L, null)
:: Nil)
+ }
+ }
+
+ Seq(false, true).foreach {
+ thinMode =>
+ test(s"skip selected-key pushdown for merge_map from $format with
thin mode $thinMode") {
+ checkMapAggregatorRead(format, thinMode,
"fields.attrs.aggregate-function", "merge_map")
+ }
+ }
+
+ Seq("fields.attrs.aggregate-function",
"fields.default-aggregate-function").foreach {
+ aggregateOption =>
+ test(
+ s"skip selected-key pushdown for custom MAP aggregator configured
by $aggregateOption from $format") {
+ checkMapAggregatorRead(format, false, aggregateOption,
"my_merge_map")
+ }
+ }
+
+ test(s"read full shared-shredding map column from $format") {
+ withTable("T") {
+ sql(s"""
+ |CREATE TABLE T (id INT, attrs MAP<STRING, BIGINT>)
+ |TBLPROPERTIES (
+ | 'bucket' = '-1',
+ | 'file.format' = '$format',
+ | 'fields.attrs.map.storage-layout' = 'shared-shredding',
+ | 'fields.attrs.map.shared-shredding.max-columns' = '1'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key1', CAST(10 AS BIGINT), 'key2', CAST(20 AS
BIGINT))),
+ | (2, map('key2', CAST(30 AS BIGINT), 'cold', CAST(40 AS
BIGINT))),
+ | (3, NULL)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql("SELECT id, attrs FROM T ORDER BY id"),
+ Row(1, Map("key1" -> 10L, "key2" -> 20L)) ::
+ Row(2, Map("key2" -> 30L, "cold" -> 40L)) ::
+ Row(3, null) :: Nil)
+ }
+ }
+
+ test(s"read selected normal map keys from $format") {
+ withTable("T") {
+ sql(s"""
+ |CREATE TABLE T (id INT, attrs MAP<STRING, BIGINT>)
+ |TBLPROPERTIES (
+ | 'bucket' = '-1',
+ | 'file.format' = '$format'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key1', CAST(10 AS BIGINT), 'key2', CAST(20 AS
BIGINT))),
+ | (2, map('key2', CAST(30 AS BIGINT), 'cold', CAST(40 AS
BIGINT))),
+ | (3, NULL)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql("SELECT id, attrs['key1'], attrs['key2'], attrs['missing']
FROM T ORDER BY id"),
+ Row(1, 10L, 20L, null) ::
+ Row(2, null, 30L, null) ::
+ Row(3, null, null, null) :: Nil
+ )
+ }
+ }
+
+ test(s"read full normal map column from $format") {
+ withTable("T") {
+ sql(s"""
+ |CREATE TABLE T (id INT, attrs MAP<STRING, BIGINT>)
+ |TBLPROPERTIES (
+ | 'bucket' = '-1',
+ | 'file.format' = '$format'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key1', CAST(10 AS BIGINT), 'key2', CAST(20 AS
BIGINT))),
+ | (2, map('key2', CAST(30 AS BIGINT), 'cold', CAST(40 AS
BIGINT))),
+ | (3, NULL)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql("SELECT id, attrs FROM T ORDER BY id"),
+ Row(1, Map("key1" -> 10L, "key2" -> 20L)) ::
+ Row(2, Map("key2" -> 30L, "cold" -> 40L)) ::
+ Row(3, null) :: Nil)
+ }
+ }
+ }
+
+ private def checkMapAggregatorRead(
+ format: String,
+ thinMode: Boolean,
+ aggregateOption: String,
+ aggregateFunction: String): Unit = {
+ withTable("T") {
+ sql(s"""
+ |CREATE TABLE T (
+ | id INT,
+ | attrs MAP<STRING, BIGINT>
+ |)
+ |TBLPROPERTIES (
+ | 'primary-key' = 'id',
+ | 'bucket' = '1',
+ | 'file.format' = '$format',
+ | 'data-file.thin-mode' = '$thinMode',
+ | 'merge-engine' = 'aggregation',
+ | '$aggregateOption' = '$aggregateFunction',
+ | 'fields.attrs.map.storage-layout' = 'shared-shredding',
+ | 'fields.attrs.map.shared-shredding.max-columns' = '1',
+ | 'write-only' = 'true'
+ |)
+ |""".stripMargin)
+
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key1', CAST(10 AS BIGINT), 'key2', CAST(20 AS
BIGINT))),
+ | (2, NULL)
+ |""".stripMargin)
+ sql("""
+ |INSERT INTO T VALUES
+ | (1, map('key2', CAST(30 AS BIGINT), 'cold', CAST(40 AS
BIGINT))),
+ | (2, map('key1', CAST(50 AS BIGINT)))
+ |""".stripMargin)
+
+ val query =
+ sql("SELECT id, attrs['key1'], attrs['key2'] FROM T ORDER BY id")
+ val sparkPlan = query.queryExecution.sparkPlan
+ val paimonScan = sparkPlan
+ .collectFirst {
+ case scan: BatchScanExec if scan.scan.isInstanceOf[PaimonScan] =>
+ scan.scan.asInstanceOf[PaimonScan]
+ }
+ .getOrElse(fail(s"Expected a Paimon scan in physical
plan:\n$sparkPlan"))
+ assert(
+ !paimonScan.pushedMapSelectedKeys.contains("attrs"),
+ s"""Expected selected-key pushdown to be skipped for
$aggregateFunction.
+ |Physical plan:
+ |$sparkPlan""".stripMargin
+ )
+
+ checkAnswer(query, Row(1, 10L, 30L) :: Row(2, 50L, null) :: Nil)
+ }
+ }
+}