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 e3e06017e0 [core] Select row sidecars using the final Data Evolution
selection (#10415)
e3e06017e0 is described below
commit e3e06017e0a255354e02630a6f834ad622217081
Author: sanshi <[email protected]>
AuthorDate: Fri Oct 16 13:10:56 2026 +0800
[core] Select row sidecars using the final Data Evolution selection (#10415)
---
.../paimon/operation/DataEvolutionSplitRead.java | 182 +++++--
.../operation/DataEvolutionSplitReadTest.java | 28 +
.../paimon/table/DataEvolutionFileIndexTest.java | 567 +++++++++++++++++++++
3 files changed, 742 insertions(+), 35 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
b/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
index b4d58af88b..783a763369 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/DataEvolutionSplitRead.java
@@ -19,10 +19,14 @@
package org.apache.paimon.operation;
import org.apache.paimon.CoreOptions;
+import org.apache.paimon.PartitionSettedRow;
import org.apache.paimon.annotation.VisibleForTesting;
import org.apache.paimon.append.ForceSingleBatchReader;
+import org.apache.paimon.casting.CastExecutors;
+import org.apache.paimon.casting.CastFieldGetter;
import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.PartitionInfo;
import org.apache.paimon.deletionvectors.ApplyDeletionVectorReader;
import org.apache.paimon.deletionvectors.DeletionVector;
import org.apache.paimon.disk.IOManager;
@@ -61,6 +65,8 @@ import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.FileStorePathFactory;
import org.apache.paimon.utils.FormatReaderMapping;
import org.apache.paimon.utils.FormatReaderMapping.Builder;
+import org.apache.paimon.utils.InternalRowUtils;
+import org.apache.paimon.utils.Pair;
import org.apache.paimon.utils.Preconditions;
import org.apache.paimon.utils.Range;
import org.apache.paimon.utils.RangeHelper;
@@ -212,7 +218,15 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
}
private RecordReader<InternalRow> createReader(
- DataSplit dataSplit, List<Range> rowRanges, RowType readRowType)
throws IOException {
+ DataSplit dataSplit, List<Range> rowRanges, RowType outputRowType)
throws IOException {
+ // Partition values belong to the split and may be absent from every
physical file.
+ // Plan and merge only data columns, then restore partitions in the
requested order.
+ Pair<Pair<int[], RowType>, List<DataField>> partitionMapping =
+ PartitionUtils.getPartitionMapping2fieldsWithoutPartition(
+ schema.partitionKeys(),
+ outputRowType.getFields(),
+ schema.logicalPartitionType());
+ RowType readRowType = new RowType(partitionMapping.getRight());
List<DataFileMeta> files = dataSplit.dataFiles();
List<DeletionFile> deletionFiles =
dataSplit.deletionFiles().orElse(null);
@@ -228,7 +242,7 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
// the suppliers below run lazily, so take the filters now, the same
way the read type is
// already taken by the caller
- List<Predicate> filters = readTypeFilters(this.filters, readRowType);
+ List<Predicate> filters = readTypeFilters(this.filters, outputRowType);
List<List<DataFileMeta>> splitByRowId = mergeRangesAndSort(files);
for (List<DataFileMeta> needMergeFiles : splitByRowId) {
@@ -317,7 +331,13 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
}
}
- return ConcatRecordReader.create(suppliers);
+ RecordReader<InternalRow> reader =
ConcatRecordReader.create(suppliers);
+ PartitionInfo partitionInfo =
PartitionUtils.create(partitionMapping.getLeft(), partition);
+ if (partitionInfo != null) {
+ PartitionSettedRow partitionRow =
PartitionSettedRow.from(partitionInfo);
+ return reader.transform(partitionRow::replaceRow);
+ }
+ return reader;
}
private RecordReader<InternalRow> createReader(IndexedSplit indexedSplit)
throws IOException {
@@ -465,7 +485,7 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
fieldsFiles.get(0),
bunchDataSchemas[0],
dataFilePathFactory,
- formatBuilder,
+ nestedFieldEnabled,
rowRanges,
readRowType,
deletionVector,
@@ -481,20 +501,23 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
}
FieldBunch bunch = fieldsFiles.get(i);
DataFileMeta firstFile = bunch.files().get(0);
- FileReadTarget readTarget = readTarget(firstFile,
dataFilePathFactory, rowRanges);
+ FileReadTarget readTarget =
+ readTarget(firstFile, dataFilePathFactory, rowRanges,
groupSelection);
String formatIdentifier = readTarget.formatIdentifier;
+ Builder targetFormatBuilder =
+ readTarget.isPositionalRowFormat()
+ ? positionalRowFormatBuilder(readRowType, null,
nestedFieldEnabled)
+ : formatBuilder;
long schemaId = firstFile.schemaId();
TableSchema dataSchema = bunchDataSchemas[i];
RowType partialReadRowType = new RowType(readFields);
List<String> cacheKey =
- nestedFieldEnabled
- ? readerCacheKey(readFields, dataSchema.fields(),
true)
- :
readFields.stream().map(DataField::name).collect(Collectors.toList());
+ readerCacheKey(readFields, dataSchema.fields(),
nestedFieldEnabled);
FormatReaderMapping formatReaderMapping =
formatReaderMappings.computeIfAbsent(
new FormatKey(schemaId, formatIdentifier,
cacheKey),
key ->
- formatBuilder.build(
+ targetFormatBuilder.build(
formatIdentifier,
schema,
dataSchema,
@@ -525,17 +548,23 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
FieldBunch bunch,
TableSchema dataSchema,
DataFilePathFactory dataFilePathFactory,
- Builder formatBuilder,
+ boolean nestedFieldEnabled,
List<Range> rowRanges,
RowType readRowType,
@Nullable DeletionVectorWithRange deletionVector,
@Nullable BitmapIndexResult groupSelection)
throws IOException {
DataFileMeta firstFile = bunch.files().get(0);
+ FileReadTarget readTarget =
+ readTarget(firstFile, dataFilePathFactory, rowRanges,
groupSelection);
+ Builder formatBuilder =
+ readTarget.isPositionalRowFormat()
+ ? positionalRowFormatBuilder(readRowType, null,
nestedFieldEnabled)
+ : formatBuilder(readRowType, null, nestedFieldEnabled);
// Use the physical schema: the full table schema may declare columns
this file never wrote.
FormatReaderMapping mapping =
formatBuilder.build(
- readTarget(firstFile, dataFilePathFactory,
rowRanges).formatIdentifier,
+ readTarget.formatIdentifier,
schema,
dataSchema,
readRowType.getFields(),
@@ -732,27 +761,14 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
@Nullable DeletionVectorWithRange deletionVector,
@Nullable FileIndexResult precomputedFileIndexResult)
throws IOException {
- FileReadTarget readTarget = readTarget(file, dataFilePathFactory,
rowRanges);
- String formatIdentifier = readTarget.formatIdentifier;
long schemaId = file.schemaId();
- TableSchema dataSchema = schemaId == schema.id() ? schema :
schemaFetcher.apply(schemaId);
+ TableSchema fileSchema = schemaId == schema.id() ? schema :
schemaFetcher.apply(schemaId);
+ TableSchema physicalDataSchema =
fileSchema.dataFileSchema(file.writeCols());
boolean nestedFieldEnabled =
nestedFieldEnabledFor(Collections.singletonList(file));
// no column merge here, so the filters this file can answer reach
both the file index and
// the format reader
List<Predicate> fileFilters = fileFilters(filters, file);
- FormatReaderMapping formatReaderMapping =
- singleFileReaderMappings.computeIfAbsent(
- new SingleFileKey(
- schemaId,
- formatIdentifier,
- file.writeCols(),
- readRowType,
- nestedFieldEnabled),
- key ->
- formatBuilder(readRowType, fileFilters,
nestedFieldEnabled)
- .build(formatIdentifier, schema,
dataSchema));
-
FileIndexResult fileIndexResult = precomputedFileIndexResult;
if (fileIndexResult == null && fileIndexReadEnabled) {
DeletionVector dv = deletionVector == null ? null :
deletionVector.deletionVector;
@@ -764,8 +780,8 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
fileIndexResult =
FileIndexEvaluator.evaluate(
fileIO,
- dataSchema,
- devolveFilters(fileFilters, dataSchema),
+ physicalDataSchema,
+ devolveFilters(fileFilters, physicalDataSchema),
null,
null,
dataFilePathFactory,
@@ -777,6 +793,27 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
}
}
+ FileReadTarget readTarget =
+ readTarget(file, dataFilePathFactory, rowRanges,
fileIndexResult);
+ String formatIdentifier = readTarget.formatIdentifier;
+ boolean positionalRowFormat = readTarget.isPositionalRowFormat();
+ Builder targetFormatBuilder =
+ positionalRowFormat
+ ? positionalRowFormatBuilder(readRowType, fileFilters,
nestedFieldEnabled)
+ : formatBuilder(readRowType, fileFilters,
nestedFieldEnabled);
+ TableSchema readerDataSchema = positionalRowFormat ?
physicalDataSchema : fileSchema;
+ FormatReaderMapping formatReaderMapping =
+ singleFileReaderMappings.computeIfAbsent(
+ new SingleFileKey(
+ schemaId,
+ formatIdentifier,
+ file.writeCols(),
+ readRowType,
+ nestedFieldEnabled),
+ key ->
+ targetFormatBuilder.build(
+ formatIdentifier, schema,
readerDataSchema));
+
return createFileReader(
partition,
file,
@@ -824,7 +861,7 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
formatReaderMapping,
rowRanges,
readRowType,
- readTarget(file, dataFilePathFactory, rowRanges),
+ readTarget(file, dataFilePathFactory, rowRanges,
groupSelection),
deletionVector,
groupSelection);
}
@@ -866,7 +903,7 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
coreOptions.scanIgnoreCorruptFile(),
coreOptions.scanIgnoreLostFile(),
formatReaderMapping.getIndexMapping(),
- formatReaderMapping.getCastMapping(),
+ trackingCastMapping(readTarget, formatReaderMapping),
PartitionUtils.create(formatReaderMapping.getPartitionPair(), partition),
true,
file.firstRowId(),
@@ -882,6 +919,32 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
fileRecordReader, file.nonNullRowIdRange(), rowRanges,
deletionVector);
}
+ @Nullable
+ private CastFieldGetter[] trackingCastMapping(
+ FileReadTarget target, FormatReaderMapping mapping) {
+ CastFieldGetter[] casts = mapping.getCastMapping();
+ if (!target.isPositionalRowFormat()
+ || casts == null
+ || mapping.getSystemFields().isEmpty()) {
+ return casts;
+ }
+ // The physical schema omits tracking fields, but DataFileRecordReader
supplies them
+ // before casting. Preserve those values without changing the cached
cast mapping.
+ CastFieldGetter[] result = casts.clone();
+ for (DataField field :
+ new DataField[] {SpecialFields.ROW_ID,
SpecialFields.SEQUENCE_NUMBER}) {
+ Integer index = mapping.getSystemFields().get(field.name());
+ if (index != null) {
+ result[index] =
+ new CastFieldGetter(
+
InternalRowUtils.createNullCheckingFieldGetter(field.type(), index),
+ CastExecutors.identityCastExecutor(),
+ field.name());
+ }
+ }
+ return result;
+ }
+
private FileRecordReader<InternalRow> applyDeletionVector(
FileRecordReader<InternalRow> reader,
Range readerRange,
@@ -1103,6 +1166,20 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
nestedFieldEnabled);
}
+ // Row sidecars and ordinary row-format data files decode against the
physical write schema.
+ // Row tracking fields are supplied by DataFileRecordReader from the
manifest entry.
+ private Builder positionalRowFormatBuilder(
+ RowType readRowType, @Nullable List<Predicate> filters, boolean
nestedFieldEnabled) {
+ return new Builder(
+ formatDiscover,
+ readRowType.getFields(),
+ TableSchema::fields,
+ filters,
+ null,
+ null,
+ nestedFieldEnabled);
+ }
+
/**
* Filters the file can answer. A column missing from a data evolution
file is not null, its
* values live in another file of the same row id range, so a predicate on
it must not be pushed
@@ -1198,13 +1275,17 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
}
private FileReadTarget readTarget(
- DataFileMeta file, DataFilePathFactory dataFilePathFactory,
List<Range> rowRanges)
+ DataFileMeta file,
+ DataFilePathFactory dataFilePathFactory,
+ List<Range> rowRanges,
+ @Nullable FileIndexResult fileIndexResult)
throws IOException {
String rowSidecar = rowSidecarFileName(file);
if (rowSidecar != null
&& shouldReadRowSidecar(
file,
rowRanges,
+ fileIndexResult,
coreOptions.dataEvolutionRowSidecarMaxSelectedRows(),
coreOptions.dataEvolutionRowSidecarMaxSelectionRatio())) {
Path rowPath = dataFilePathFactory.toAlignedPath(rowSidecar, file);
@@ -1238,6 +1319,7 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
return shouldReadRowSidecar(
file,
rowRanges,
+ null,
CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_MAX_SELECTED_ROWS.defaultValue(),
CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_MAX_SELECTION_RATIO.defaultValue());
}
@@ -1248,16 +1330,24 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
@Nullable List<Range> rowRanges,
long maxSelectedRows,
double maxSelectionRatio) {
- if (rowRanges == null
- || rowRanges.isEmpty()
- || file.rowCount() <= 0
+ return shouldReadRowSidecar(file, rowRanges, null, maxSelectedRows,
maxSelectionRatio);
+ }
+
+ @VisibleForTesting
+ static boolean shouldReadRowSidecar(
+ DataFileMeta file,
+ @Nullable List<Range> rowRanges,
+ @Nullable FileIndexResult fileIndexResult,
+ long maxSelectedRows,
+ double maxSelectionRatio) {
+ if (file.rowCount() <= 0
|| isBlobFile(file.fileName())
|| isVectorStoreFile(file.fileName())
|| rowSidecarFileName(file) == null) {
return false;
}
- long selectedRowCount = selectedRowCount(file, rowRanges);
+ long selectedRowCount = selectedRowCount(file, rowRanges,
fileIndexResult);
if (selectedRowCount <= 0 || selectedRowCount >= file.rowCount()) {
return false;
}
@@ -1281,6 +1371,20 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
.sum();
}
+ private static long selectedRowCount(
+ DataFileMeta file,
+ @Nullable List<Range> rowRanges,
+ @Nullable FileIndexResult fileIndexResult) {
+ if (fileIndexResult instanceof BitmapIndexResult) {
+ RoaringBitmap32 indexSelection = ((BitmapIndexResult)
fileIndexResult).get();
+ RoaringBitmap32 rangeSelection = file.toFileSelection(rowRanges);
+ return rangeSelection == null
+ ? indexSelection.getCardinality()
+ : RoaringBitmap32.and(rangeSelection,
indexSelection).getCardinality();
+ }
+ return isNullOrEmpty(rowRanges) ? file.rowCount() :
selectedRowCount(file, rowRanges);
+ }
+
private static boolean isRowSidecarFile(String fileName) {
try {
return
ROW_SIDECAR_FORMAT.equals(DataFilePathFactory.formatIdentifier(fileName));
@@ -1300,6 +1404,14 @@ public class DataEvolutionSplitRead implements
SplitRead<InternalRow> {
this.path = path;
this.fileSize = fileSize;
}
+
+ /**
+ * Sidecars and ordinary "row" data files use the same positional
decoder and physical
+ * schema mapping. They can share a cache entry for the same physical
and requested fields.
+ */
+ private boolean isPositionalRowFormat() {
+ return ROW_SIDECAR_FORMAT.equals(formatIdentifier);
+ }
}
/**
diff --git
a/paimon-core/src/test/java/org/apache/paimon/operation/DataEvolutionSplitReadTest.java
b/paimon-core/src/test/java/org/apache/paimon/operation/DataEvolutionSplitReadTest.java
index cd60d98164..2a1c5c4472 100644
---
a/paimon-core/src/test/java/org/apache/paimon/operation/DataEvolutionSplitReadTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/operation/DataEvolutionSplitReadTest.java
@@ -23,6 +23,7 @@ import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericArray;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.fileindex.bitmap.BitmapIndexResult;
import org.apache.paimon.format.FileFormat;
import org.apache.paimon.format.FormatWriter;
import org.apache.paimon.fs.Path;
@@ -42,6 +43,7 @@ import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.FileStorePathFactory;
import org.apache.paimon.utils.Range;
+import org.apache.paimon.utils.RoaringBitmap32;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -225,6 +227,32 @@ class DataEvolutionSplitReadTest {
smallFile, Collections.singletonList(new Range(10L,
15L)), 64L, 0.25d));
}
+ @Test
+ public void testShouldReadRowSidecarUsesFinalBitmapSelection() {
+ DataFileMeta file =
+ createFile("file1.parquet", 10L, 100, 1,
Collections.singletonList("file1.row"));
+ BitmapIndexResult bitmap = new BitmapIndexResult(() ->
RoaringBitmap32.bitmapOf(0, 5, 20));
+ BitmapIndexResult singleRowBitmap =
+ new BitmapIndexResult(() -> RoaringBitmap32.bitmapOf(50));
+ List<Range> rowRanges = Collections.singletonList(new Range(10L, 19L));
+
+ assertTrue(DataEvolutionSplitRead.shouldReadRowSidecar(file,
rowRanges, bitmap, 2L, 0.02d));
+ assertTrue(
+ DataEvolutionSplitRead.shouldReadRowSidecar(
+ file, null, singleRowBitmap, 1L, 0.01d));
+ assertTrue(
+ DataEvolutionSplitRead.shouldReadRowSidecar(
+ file,
+ Collections.singletonList(new Range(10L, 109L)),
+ singleRowBitmap,
+ 1L,
+ 0.01d));
+ assertFalse(
+ DataEvolutionSplitRead.shouldReadRowSidecar(file, rowRanges,
bitmap, 1L, 0.02d));
+ assertFalse(
+ DataEvolutionSplitRead.shouldReadRowSidecar(file, rowRanges,
bitmap, 2L, 0.01d));
+ }
+
@Test
public void testShouldNotReadRowSidecarWithoutSparseSelection() {
DataFileMeta file =
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
index be205c1e3a..1e7d086679 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/DataEvolutionFileIndexTest.java
@@ -37,9 +37,12 @@ import
org.apache.paimon.fileindex.bitmap.BitmapFileIndexFactory;
import org.apache.paimon.fileindex.bloomfilter.BloomFilterFileIndexFactory;
import org.apache.paimon.fileindex.bsi.BitSliceIndexBitmapFileIndexFactory;
import org.apache.paimon.fs.Path;
+import org.apache.paimon.globalindex.IndexedSplit;
import org.apache.paimon.index.IndexFileMeta;
+import org.apache.paimon.io.BundleRecords;
import org.apache.paimon.io.CompactIncrement;
import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.io.DataFilePathFactory;
import org.apache.paimon.io.DataIncrement;
import org.apache.paimon.manifest.FileKind;
import org.apache.paimon.manifest.IndexManifestEntry;
@@ -75,6 +78,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -114,6 +118,485 @@ public class DataEvolutionFileIndexTest extends
DataEvolutionTestBase {
/** Inside the min/max range of f2 ("b000".."b099"), but not written. */
private static final String MISSING_F2 = "b050x";
+ @Test
+ public void testBitmapFilterWithProjectedWriteSchemaAndRowSidecar() throws
Exception {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("projected_schema_row_sidecar",
options);
+ writeSplitColumns(table, ROW_COUNT, Collections.emptyMap(),
bitmapOptions("f2"));
+
+ FileStoreTable latest = getTable(identifier(table.name()));
+ RowType projection = latest.rowType().project("f2");
+ Predicate filter = equalF2(f2(50));
+ ReadBuilder readBuilder =
+
latest.newReadBuilder().withReadType(projection).withFilter(filter);
+ List<InternalRow> rows =
+ collect(
+ readBuilder.newRead().executeFilter(),
+ readBuilder.newScan().plan(),
+ projection);
+
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getString(0).toString()).isEqualTo(f2(50));
+
+ ReadBuilder rangeReadBuilder =
+ latest.newReadBuilder()
+ .withReadType(projection)
+ .withFilter(filter)
+ .withRowRanges(Collections.singletonList(new Range(0L,
ROW_COUNT - 1L)));
+ rows =
+ collect(
+ rangeReadBuilder.newRead().executeFilter(),
+ rangeReadBuilder.newScan().plan(),
+ projection);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getString(0).toString()).isEqualTo(f2(50));
+
+ RowType rowIdType = rowTypeWithRowId(latest.rowType());
+ RowType rowIdProjection =
rowIdType.project(SpecialFields.ROW_ID.name(), "f2");
+ PredicateBuilder rowIdBuilder = new PredicateBuilder(rowIdType);
+ Predicate rowIdFilter =
+ PredicateBuilder.and(
+ rowIdBuilder.between(3, 0L, ROW_COUNT - 1L),
+ rowIdBuilder.equal(2,
BinaryString.fromString(f2(50))));
+ ReadBuilder rowIdReadBuilder =
+
latest.newReadBuilder().withReadType(rowIdProjection).withFilter(rowIdFilter);
+ rows =
+ collect(
+ rowIdReadBuilder.newRead().executeFilter(),
+ rowIdReadBuilder.newScan().plan(),
+ rowIdProjection);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getLong(0)).isEqualTo(50L);
+ assertThat(rows.get(0).getString(1).toString()).isEqualTo(f2(50));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void
testPositionalRowPreservesTrackingFieldsAfterSchemaCast(boolean
ordinaryRowFormat)
+ throws Exception {
+ Map<String, String> options = bitmapOptions("f1");
+ options.put(
+ CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
+ Boolean.toString(!ordinaryRowFormat));
+ if (ordinaryRowFormat) {
+ options.put(CoreOptions.FILE_FORMAT.key(), "row");
+ }
+ FileStoreTable table = createTable("row_sidecar_tracking_cast",
options);
+ writeAllColumns(table, ROW_COUNT);
+ catalog.alterTable(
+ identifier("row_sidecar_tracking_cast"),
+ SchemaChange.updateColumnType("f0", DataTypes.BIGINT()),
+ false);
+
+ FileStoreTable latest =
getTable(identifier("row_sidecar_tracking_cast"));
+ RowType readType =
+ SpecialFields.rowTypeWithRowTracking(latest.rowType(), true,
true)
+ .project(
+ "f0",
+ SpecialFields.ROW_ID.name(),
+ SpecialFields.SEQUENCE_NUMBER.name());
+ Predicate filter =
+ new PredicateBuilder(latest.rowType()).equal(1,
BinaryString.fromString(f1(50)));
+
+ ReadBuilder readBuilder =
latest.newReadBuilder().withReadType(readType).withFilter(filter);
+ TableScan.Plan plan = readBuilder.newScan().plan();
+ long maxSequenceNumber =
+ ((DataSplit)
plan.splits().get(0)).dataFiles().get(0).maxSequenceNumber();
+ List<InternalRow> rows =
collect(readBuilder.newRead().executeFilter(), plan, readType);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getLong(0)).isEqualTo(50L);
+ assertThat(rows.get(0).getLong(1)).isEqualTo(50L);
+ assertThat(rows.get(0).isNullAt(2)).isFalse();
+ assertThat(rows.get(0).getLong(2)).isEqualTo(maxSequenceNumber);
+
+ ReadBuilder rangeReadBuilder =
+ latest.newReadBuilder()
+ .withReadType(readType)
+ .withFilter(filter)
+ .withRowRanges(Collections.singletonList(new Range(0L,
ROW_COUNT - 1L)));
+ rows =
+ collect(
+ rangeReadBuilder.newRead().executeFilter(),
+ rangeReadBuilder.newScan().plan(),
+ readType);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getLong(0)).isEqualTo(50L);
+ assertThat(rows.get(0).getLong(1)).isEqualTo(50L);
+ assertThat(rows.get(0).isNullAt(2)).isFalse();
+ assertThat(rows.get(0).getLong(2)).isEqualTo(maxSequenceNumber);
+
+ RowType trackedType =
SpecialFields.rowTypeWithRowTracking(latest.rowType(), true, true);
+ PredicateBuilder trackedBuilder = new PredicateBuilder(trackedType);
+ Predicate rowIdFilter =
+ PredicateBuilder.and(
+ filter,
+ trackedBuilder.between(
+
trackedType.getFieldIndex(SpecialFields.ROW_ID.name()),
+ 0L,
+ ROW_COUNT - 1L));
+ ReadBuilder rowIdReadBuilder =
+
latest.newReadBuilder().withReadType(readType).withFilter(rowIdFilter);
+ rows =
+ collect(
+ rowIdReadBuilder.newRead().executeFilter(),
+ rowIdReadBuilder.newScan().plan(),
+ readType);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getLong(0)).isEqualTo(50L);
+ assertThat(rows.get(0).getLong(1)).isEqualTo(50L);
+ assertThat(rows.get(0).getLong(2)).isEqualTo(maxSequenceNumber);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testRowSidecarRestoresMissingPartitionField(boolean merged)
throws Exception {
+ Schema.Builder schemaBuilder =
+ Schema.newBuilder()
+ .column("pt", DataTypes.STRING())
+ .column("f1", DataTypes.STRING())
+ .column("f2", DataTypes.STRING())
+ .partitionKeys("pt")
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(),
"true")
+
.option(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(), "true");
+ Identifier identifier = identifier("row_sidecar_partition_restore");
+ catalog.createTable(identifier, schemaBuilder.build(), false);
+ FileStoreTable table = getTable(identifier);
+
+ RowType writeType = table.rowType().project("f1");
+ BatchWriteBuilder writeBuilder =
table.copy(bitmapOptions("f1")).newBatchWriteBuilder();
+ List<InternalRow> writtenRows = new ArrayList<>();
+ for (int i = 0; i < ROW_COUNT; i++) {
+ writtenRows.add(GenericRow.of(BinaryString.fromString(f1(i))));
+ }
+ try (BatchTableWrite write =
writeBuilder.newWrite().withWriteType(writeType);
+ BatchTableCommit commit = writeBuilder.newCommit()) {
+ write.writeBundle(
+ new
InternalRowSerializer(table.schema().logicalPartitionType())
+
.toBinaryRow(GenericRow.of(BinaryString.fromString("p1"))),
+ 0,
+ new BundleRecords() {
+ @Override
+ public long rowCount() {
+ return writtenRows.size();
+ }
+
+ @Override
+ public Iterator<InternalRow> iterator() {
+ return writtenRows.iterator();
+ }
+ });
+ commit.commit(write.prepareCommit());
+ }
+
+ if (merged) {
+ BatchWriteBuilder mergeWriteBuilder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write =
+ mergeWriteBuilder
+ .newWrite()
+
.withWriteType(table.rowType().project("f2"));
+ BatchTableCommit commit = mergeWriteBuilder.newCommit()) {
+ write.writeBundle(
+ new
InternalRowSerializer(table.schema().logicalPartitionType())
+
.toBinaryRow(GenericRow.of(BinaryString.fromString("p1"))),
+ 0,
+ new BundleRecords() {
+ @Override
+ public long rowCount() {
+ return ROW_COUNT;
+ }
+
+ @Override
+ public Iterator<InternalRow> iterator() {
+ return writtenRows.stream()
+ .map(row -> (InternalRow)
GenericRow.of(row.getString(0)))
+ .iterator();
+ }
+ });
+ List<CommitMessage> messages = write.prepareCommit();
+ setFirstRowId(messages, 0L);
+ commit.commit(messages);
+ }
+ }
+
+ FileStoreTable latest = getTable(identifier);
+ RowType readType =
+ latest.rowType()
+ .project(
+ merged
+ ? new String[] {"f2", "pt", "f1"}
+ : new String[] {"pt", "f1"});
+ Predicate filter =
+ new PredicateBuilder(latest.rowType()).equal(1,
BinaryString.fromString(f1(50)));
+ for (List<Range> ranges : Arrays.<List<Range>>asList(null,
fullRowRanges())) {
+ ReadBuilder readBuilder =
+
latest.newReadBuilder().withReadType(readType).withFilter(filter);
+ if (ranges != null) {
+ readBuilder.withRowRanges(ranges);
+ }
+ List<InternalRow> rows =
+ collect(
+ readBuilder.newRead().executeFilter(),
+ readBuilder.newScan().plan(),
+ readType);
+
+ InternalRow row = assertSingleRow(rows);
+
assertThat(row.getString(readType.getFieldIndex("pt")).toString()).isEqualTo("p1");
+
assertThat(row.getString(readType.getFieldIndex("f1")).toString()).isEqualTo(f1(50));
+ if (merged) {
+ assertThat(row.getString(0).toString()).isEqualTo(f1(50));
+ }
+ }
+
+ RowType partitionOnly = latest.rowType().project("pt");
+ ReadBuilder partitionReadBuilder =
latest.newReadBuilder().withReadType(partitionOnly);
+ List<InternalRow> partitions =
+ collect(
+ partitionReadBuilder.newRead(),
+ partitionReadBuilder.newScan().plan(),
+ partitionOnly);
+ assertThat(partitions).hasSize(ROW_COUNT);
+ assertThat(partitions)
+ .allSatisfy(row ->
assertThat(row.getString(0).toString()).isEqualTo("p1"));
+ }
+
+ // Deleting the data files proves the rows come from the row sidecars:
without them the read
+ // would fail on the missing files.
+ @Test
+ public void testMergedGroupFinalSelectionChoosesRowSidecar() throws
Exception {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("merged_bitmap_dv_row_sidecar",
options);
+ writeSplitColumns(table, ROW_COUNT, bitmapOptions("f1"),
Collections.emptyMap());
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ assertRow(assertSingleRow(readAfterDeletingDataFiles(table, filter,
null)), 50);
+ }
+
+ @Test
+ public void testSingleFileFinalSelectionChoosesRowSidecar() throws
Exception {
+ Map<String, String> options = bitmapOptions("f1");
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("single_bitmap_dv_row_sidecar",
options);
+ writeAllColumns(table, ROW_COUNT);
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ assertRow(assertSingleRow(readAfterDeletingDataFiles(table, filter,
null)), 50);
+ }
+
+ @Test
+ public void testMergedGroupFinalSelectionChoosesRowSidecarWithRowRanges()
throws Exception {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table =
createTable("merged_bitmap_dv_row_sidecar_ranges", options);
+ writeSplitColumns(table, ROW_COUNT, bitmapOptions("f1"),
Collections.emptyMap());
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ assertRow(assertSingleRow(readAfterDeletingDataFiles(table, filter,
fullRowRanges())), 50);
+ }
+
+ @Test
+ public void testSingleFileFinalSelectionChoosesRowSidecarWithRowRanges()
throws Exception {
+ Map<String, String> options = bitmapOptions("f1");
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table =
createTable("single_bitmap_dv_row_sidecar_ranges", options);
+ writeAllColumns(table, ROW_COUNT);
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ assertRow(assertSingleRow(readAfterDeletingDataFiles(table, filter,
fullRowRanges())), 50);
+ }
+
+ private static List<Range> fullRowRanges() {
+ return Collections.singletonList(new Range(0L, ROW_COUNT - 1L));
+ }
+
+ // The f0/f1 file is read from its row sidecar (its data file is deleted),
the f2 file has no
+ // sidecar and is read from its data file.
+ @Test
+ public void testMergedGroupMixedSidecarAndDataFileRead() throws Exception {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("merged_mixed_sidecar_data_file",
options);
+ Map<String, String> withoutSidecar = new HashMap<>();
+
withoutSidecar.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"false");
+ writeSplitColumns(table, ROW_COUNT, bitmapOptions("f1"),
withoutSidecar);
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ assertRow(
+ assertSingleRow(
+ readAfterDeletingDataFiles(
+ table,
+ filter,
+ null,
+ file ->
+ file.writeCols() != null
+ &&
file.writeCols().contains("f1"))),
+ 50);
+ }
+
+ // The first file is read from its row sidecar (its data file is deleted),
the second file has
+ // no sidecar and is read from its data file.
+ @Test
+ public void testSingleFileMixedSidecarAndDataFileRead() throws Exception {
+ Map<String, String> options = bitmapOptions("f1");
+ options.put(CoreOptions.DELETION_VECTORS_ENABLED.key(), "true");
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("single_mixed_sidecar_data_file",
options);
+ writeAllColumns(table, ROW_COUNT);
+ Map<String, String> withoutSidecar = new
HashMap<>(bitmapOptions("f1"));
+
withoutSidecar.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"false");
+
writeAllColumns(getTable(identifier(table.name())).copy(withoutSidecar),
ROW_COUNT);
+ // deleting the overlapping rows leaves the first file with a
single-row bitmap selection,
+ // sparse enough to be read from its sidecar, while the second file
has no sidecar at all
+ deleteRows(table, 51, 52, 53, 54, 55);
+
+ Predicate filter = equalAnyF1(50, 51, 52, 53, 54, 55);
+ List<InternalRow> rows =
+ readAfterDeletingDataFiles(
+ table,
+ filter,
+ null,
+ file -> file.firstRowId() != null && file.firstRowId()
== 0L);
+ // row 50 of the first file comes from its sidecar, rows 50-55 of the
second file come
+ // from its data file
+ assertThat(rows).hasSize(7);
+ for (InternalRow row : rows) {
+ int index = row.getInt(0);
+ assertThat(index).isBetween(50, 55);
+ assertThat(row.getString(1).toString()).isEqualTo(f1(index));
+ assertThat(row.getString(2).toString()).isEqualTo(f2(index));
+ }
+
+ // the same mixed read with row tracking: the row id of the sidecar
row comes from the
+ // manifest entry, the row ids of the data file rows from its own range
+ RowType readType =
+ rowTypeWithRowId(getTable(identifier(table.name())).rowType())
+ .project(SpecialFields.ROW_ID.name(), "f0", "f1");
+ List<InternalRow> trackedRows = readWithFilter(table, filter,
readType);
+ assertThat(trackedRows).hasSize(7);
+ assertThat(trackedRows.stream().map(row -> row.getLong(0)))
+ .containsExactlyInAnyOrder(50L, 150L, 151L, 152L, 153L, 154L,
155L);
+ }
+
+ // Ordinary row-format data files use the same physical schema mapping as
row sidecars. The
+ // row format has no format-level filter pushdown, so the bitmap index
provides the selection.
+ @Test
+ public void testOrdinaryRowFormatDataFile() throws Exception {
+ Map<String, String> options = bitmapOptions("f1");
+ options.put(CoreOptions.FILE_FORMAT.key(), "row");
+ FileStoreTable table = createTable("ordinary_row_format_file",
options);
+ writeAllColumns(table, ROW_COUNT);
+
+ assertRow(assertSingleRow(readWithFilter(table, equalF1(f1(50)),
null)), 50);
+
+ RowType readType =
+
rowTypeWithRowId(table.rowType()).project(SpecialFields.ROW_ID.name(), "f1");
+ List<InternalRow> rows = readWithFilter(table, equalF1(f1(50)),
readType);
+ assertThat(rows).hasSize(1);
+ assertThat(rows.get(0).getLong(0)).isEqualTo(50L);
+ assertThat(rows.get(0).getString(1).toString()).isEqualTo(f1(50));
+ }
+
+ @Test
+ public void testFormatReaderCacheKeyIncludesPhysicalSchema() throws
Exception {
+ Map<String, String> options = new HashMap<>();
+ options.put(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ FileStoreTable table = createTable("row_sidecar_physical_schema_key",
options);
+ writeSplitColumns(table, ROW_COUNT, Collections.emptyMap(),
bitmapOptions("f2"));
+ writeSecondSplitColumns(table, ROW_COUNT, bitmapOptions("f2"));
+
+ FileStoreTable latest = getTable(identifier(table.name()));
+ RowType projection = latest.rowType().project(Arrays.asList("f1",
"f2"));
+ List<InternalRow> rows = readWithFilter(table, equalF2(f2(50)),
projection);
+
+ assertThat(rows).hasSize(2);
+ assertThat(rows)
+ .extracting(row -> row.getString(0).toString())
+ .containsExactlyInAnyOrder(f1(50), "second-" + f1(50));
+ assertThat(rows).extracting(row ->
row.getString(1).toString()).containsOnly(f2(50));
+ }
+
+ @Test
+ public void testBitmapFilterWithWideWriteSchemaAndRowSidecar() throws
Exception {
+ Schema.Builder schemaBuilder = Schema.newBuilder();
+ for (int i = 0; i < 7; i++) {
+ schemaBuilder.column("c" + i, DataTypes.INT());
+ }
+ schemaBuilder
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ROW_SIDECAR_ENABLED.key(),
"true");
+ bitmapOptions("c6").forEach(schemaBuilder::option);
+ Identifier identifier = identifier("wide_schema_row_sidecar");
+ catalog.createTable(identifier, schemaBuilder.build(), false);
+ FileStoreTable table = getTable(identifier);
+
+ BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = writeBuilder.newWrite();
+ BatchTableCommit commit = writeBuilder.newCommit()) {
+ for (int i = 0; i < ROW_COUNT; i++) {
+ write.write(GenericRow.of(i, i, i, i, i, i, i));
+ }
+ commit.commit(write.prepareCommit());
+ }
+
+ Predicate filter = new PredicateBuilder(table.rowType()).equal(6, 50);
+ ReadBuilder readBuilder = table.newReadBuilder().withFilter(filter);
+ List<InternalRow> rows =
+ collect(
+ readBuilder.newRead().executeFilter(),
+ readBuilder.newScan().plan(),
+ table.rowType());
+
+ InternalRow row = assertSingleRow(rows);
+ for (int i = 0; i < 7; i++) {
+ assertThat(row.getInt(i)).isEqualTo(50);
+ }
+
+ ReadBuilder rangeReadBuilder =
+ table.newReadBuilder()
+ .withFilter(filter)
+ .withRowRanges(Collections.singletonList(new Range(0L,
ROW_COUNT - 1L)));
+ rows =
+ collect(
+ rangeReadBuilder.newRead().executeFilter(),
+ rangeReadBuilder.newScan().plan(),
+ table.rowType());
+ row = assertSingleRow(rows);
+ for (int i = 0; i < 7; i++) {
+ assertThat(row.getInt(i)).isEqualTo(50);
+ }
+
+ RowType rowIdType = rowTypeWithRowId(table.rowType());
+ PredicateBuilder rowIdBuilder = new PredicateBuilder(rowIdType);
+ Predicate rowIdFilter =
+ PredicateBuilder.and(
+ rowIdBuilder.between(7, 0L, ROW_COUNT - 1L),
rowIdBuilder.equal(6, 50));
+ ReadBuilder rowIdReadBuilder =
+
table.newReadBuilder().withReadType(rowIdType).withFilter(rowIdFilter);
+ rows =
+ collect(
+ rowIdReadBuilder.newRead().executeFilter(),
+ rowIdReadBuilder.newScan().plan(),
+ rowIdType);
+ row = assertSingleRow(rows);
+ for (int i = 0; i < 7; i++) {
+ assertThat(row.getInt(i)).isEqualTo(50);
+ }
+ assertThat(row.getLong(7)).isEqualTo(50L);
+ }
+
@Test
public void testSingleFileSkippedByFileIndex() throws Exception {
// a standalone .index file, only the reader can evaluate it
@@ -1048,6 +1531,35 @@ public class DataEvolutionFileIndexTest extends
DataEvolutionTestBase {
}
}
+ /** Writes f1 and f2 into a second merged group with a different physical
schema. */
+ private void writeSecondSplitColumns(
+ FileStoreTable table, int count, Map<String, String>
secondOptions) throws Exception {
+ FileStoreTable latest = getTable(identifier(table.name()));
+ RowType writeType0 =
latest.rowType().project(Collections.singletonList("f1"));
+ BatchWriteBuilder builder = latest.newBatchWriteBuilder();
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType0);
+ BatchTableCommit commit = builder.newCommit()) {
+ for (int i = 0; i < count; i++) {
+ write.write(GenericRow.of(BinaryString.fromString("second-" +
f1(i))));
+ }
+ commit.commit(write.prepareCommit());
+ }
+
+ latest = getTable(identifier(table.name()));
+ long firstRowId =
latest.snapshotManager().latestSnapshot().nextRowId() - count;
+ RowType writeType1 =
latest.rowType().project(Collections.singletonList("f2"));
+ builder = latest.copy(secondOptions).newBatchWriteBuilder();
+ try (BatchTableWrite write =
builder.newWrite().withWriteType(writeType1);
+ BatchTableCommit commit = builder.newCommit()) {
+ for (int i = 0; i < count; i++) {
+ write.write(GenericRow.of(BinaryString.fromString(f2(i))));
+ }
+ List<CommitMessage> commitables = write.prepareCommit();
+ setSingleFileFirstRowId(commitables, firstRowId);
+ commit.commit(commitables);
+ }
+ }
+
/**
* Writes f0/f1 (a*) with a bloom index on f1, then overwrites f1 with c*
in a second file that
* shares the same row id range and has a higher sequence number, so the
column merge takes f1
@@ -1097,6 +1609,53 @@ public class DataEvolutionFileIndexTest extends
DataEvolutionTestBase {
return readWithFilter(table, predicate, null);
}
+ private List<InternalRow> readAfterDeletingDataFiles(
+ FileStoreTable table, Predicate predicate, @Nullable List<Range>
rowRanges)
+ throws Exception {
+ return readAfterDeletingDataFiles(table, predicate, rowRanges, null);
+ }
+
+ /**
+ * Reads after deleting the data files. A non-null delete filter restricts
the deletion to the
+ * matching files, so part of the split is read from the row sidecars and
the rest from the
+ * remaining data files.
+ */
+ private List<InternalRow> readAfterDeletingDataFiles(
+ FileStoreTable table,
+ Predicate predicate,
+ @Nullable List<Range> rowRanges,
+ @Nullable java.util.function.Predicate<DataFileMeta> deleteFilter)
+ throws Exception {
+ FileStoreTable latest = getTable(identifier(table.name()));
+ ReadBuilder readBuilder =
latest.newReadBuilder().withFilter(predicate);
+ if (rowRanges != null) {
+ readBuilder.withRowRanges(rowRanges);
+ }
+ TableScan.Plan plan = readBuilder.newScan().plan();
+ // with row ranges the scan wraps the split into an IndexedSplit
+ Split first = plan.splits().get(0);
+ DataSplit split =
+ first instanceof IndexedSplit
+ ? ((IndexedSplit) first).dataSplit()
+ : (DataSplit) first;
+ DataFilePathFactory pathFactory =
+ latest.store()
+ .pathFactory()
+ .createDataFilePathFactory(split.partition(),
split.bucket());
+ for (DataFileMeta file : split.dataFiles()) {
+ if (deleteFilter == null || deleteFilter.test(file)) {
+ assertThat(latest.fileIO().delete(pathFactory.toPath(file),
false)).isTrue();
+ }
+ }
+
+ List<InternalRow> rows = new ArrayList<>();
+ InternalRowSerializer serializer = new
InternalRowSerializer(latest.rowType());
+ try (RecordReader<InternalRow> reader =
readBuilder.newRead().createReader(plan)) {
+ reader.forEachRemaining(row -> rows.add(serializer.copy(row)));
+ }
+ return rows;
+ }
+
private List<InternalRow> readWithFilter(
FileStoreTable table, Predicate predicate, @Nullable RowType
readType)
throws Exception {
@@ -1252,6 +1811,14 @@ public class DataEvolutionFileIndexTest extends
DataEvolutionTestBase {
return new PredicateBuilder(rowType()).equal(1,
BinaryString.fromString(value));
}
+ private static Predicate equalAnyF1(int... values) {
+ Predicate[] predicates = new Predicate[values.length];
+ for (int i = 0; i < values.length; i++) {
+ predicates[i] = equalF1(f1(values[i]));
+ }
+ return PredicateBuilder.or(predicates);
+ }
+
private static Predicate equalF2(String value) {
return new PredicateBuilder(rowType()).equal(2,
BinaryString.fromString(value));
}