Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@
/** Immutable sorted-index state for one field and bucket. */
public final class PkSortedBucketIndexState {

private static final class PayloadCandidate {

private final IndexFileMeta payload;
private final PkSortedIndexGroup group;
private final List<PrimaryKeyIndexSourceFile> activeSources;

private PayloadCandidate(
IndexFileMeta payload,
PkSortedIndexGroup group,
List<PrimaryKeyIndexSourceFile> activeSources) {
this.payload = payload;
this.group = group;
this.activeSources = activeSources;
}
}

private final List<PkSortedIndexGroup> groups;
private final List<PrimaryKeyIndexSourceFile> coveredSourceFiles;
private final List<PrimaryKeyIndexSourceFile> uncoveredSourceFiles;
Expand Down Expand Up @@ -72,55 +88,102 @@ public static PkSortedBucketIndexState fromActiveDataFiles(
sources.sort(Comparator.comparing(PrimaryKeyIndexSourceFile::fileName));
}

Map<Integer, List<IndexFileMeta>> payloadsByLevel = new TreeMap<>();
Map<Integer, List<PayloadCandidate>> candidatesByLevel = new TreeMap<>();
List<IndexFileMeta> rejected = new ArrayList<>();
for (IndexFileMeta payload : activePayloads) {
try {
PrimaryKeyIndexSourceMeta sourceMeta =
PrimaryKeyIndexSourceMeta.fromIndexFile(payload);
List<PrimaryKeyIndexSourceFile> desired =
List<PrimaryKeyIndexSourceFile> activeLevelSources =
sourcesByLevel.get(sourceMeta.dataLevel());
if (desired == null || !desired.equals(sourceMeta.sourceFiles())) {
if (activeLevelSources == null) {
rejected.add(payload);
continue;
}

List<PrimaryKeyIndexSourceFile> payloadSources = sourceMeta.sourceFiles();
List<PrimaryKeyIndexSourceFile> activeIntersection =
activeIntersection(activeLevelSources, payloadSources);
if (activeIntersection == null || activeIntersection.isEmpty()) {
rejected.add(payload);
continue;
}

Optional<PkSortedIndexGroup> group =
PkSortedIndexGroup.create(
fieldId,
indexType,
payloadSources,
Collections.singletonList(payload));
if (!group.isPresent()) {
rejected.add(payload);
} else {
payloadsByLevel
.computeIfAbsent(sourceMeta.dataLevel(), ignored -> new ArrayList<>())
.add(payload);
continue;
}
candidatesByLevel
.computeIfAbsent(sourceMeta.dataLevel(), ignored -> new ArrayList<>())
.add(new PayloadCandidate(payload, group.get(), activeIntersection));
} catch (RuntimeException ignored) {
rejected.add(payload);
}
}

List<PkSortedIndexGroup> groups = new ArrayList<>();
Set<Integer> coveredLevels = new HashSet<>();
for (Map.Entry<Integer, List<IndexFileMeta>> entry : payloadsByLevel.entrySet()) {
List<IndexFileMeta> levelPayloads = entry.getValue();
Optional<PkSortedIndexGroup> group =
levelPayloads.size() == 1
? PkSortedIndexGroup.create(
fieldId,
indexType,
sourcesByLevel.get(entry.getKey()),
levelPayloads)
: Optional.empty();
if (group.isPresent()) {
groups.add(group.get());
coveredLevels.add(entry.getKey());
} else {
rejected.addAll(levelPayloads);
Map<Integer, Set<PrimaryKeyIndexSourceFile>> coveredSourcesByLevel = new TreeMap<>();
for (Map.Entry<Integer, List<PayloadCandidate>> entry : candidatesByLevel.entrySet()) {
List<PayloadCandidate> levelCandidates = entry.getValue();
if (levelCandidates.size() != 1) {
for (PayloadCandidate candidate : levelCandidates) {
rejected.add(candidate.payload);
}
continue;
}
PayloadCandidate candidate = levelCandidates.get(0);
groups.add(candidate.group);
coveredSourcesByLevel
.computeIfAbsent(entry.getKey(), ignored -> new HashSet<>())
.addAll(candidate.activeSources);
}

List<PrimaryKeyIndexSourceFile> covered = new ArrayList<>();
List<PrimaryKeyIndexSourceFile> uncovered = new ArrayList<>();
for (Map.Entry<Integer, List<PrimaryKeyIndexSourceFile>> entry :
sourcesByLevel.entrySet()) {
(coveredLevels.contains(entry.getKey()) ? covered : uncovered).addAll(entry.getValue());
Set<PrimaryKeyIndexSourceFile> coveredSources =
coveredSourcesByLevel.getOrDefault(entry.getKey(), Collections.emptySet());
for (PrimaryKeyIndexSourceFile source : entry.getValue()) {
(coveredSources.contains(source) ? covered : uncovered).add(source);
}
}
return new PkSortedBucketIndexState(groups, covered, uncovered, rejected);
}

private static List<PrimaryKeyIndexSourceFile> activeIntersection(
List<PrimaryKeyIndexSourceFile> activeSources,
List<PrimaryKeyIndexSourceFile> payloadSources) {
List<PrimaryKeyIndexSourceFile> intersection = new ArrayList<>();
int activeSourceIndex = 0;
for (int i = 0; i < payloadSources.size(); i++) {
PrimaryKeyIndexSourceFile source = payloadSources.get(i);
if (i > 0 && payloadSources.get(i - 1).fileName().compareTo(source.fileName()) >= 0) {
return null;
}
while (activeSourceIndex < activeSources.size()
&& activeSources.get(activeSourceIndex).fileName().compareTo(source.fileName())
< 0) {
activeSourceIndex++;
}
if (activeSourceIndex == activeSources.size()
|| !activeSources.get(activeSourceIndex).fileName().equals(source.fileName())) {
continue;
}
if (activeSources.get(activeSourceIndex).rowCount() != source.rowCount()) {
return null;
}
intersection.add(source);
}
return intersection;
}

public List<PkSortedIndexGroup> groups() {
return groups;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
import java.util.Optional;
import java.util.Set;

/** The single payload which indexes one complete data level. */
/**
* One validated payload which indexes an immutable source group at one data level.
* Snapshot-specific active coverage is validated by {@link PkSortedBucketIndexState}.
*/
public final class PkSortedIndexGroup {

private final int dataLevel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.paimon.index.IndexPathFactory;
import org.apache.paimon.index.pk.PrimaryKeyIndexDefinition;
import org.apache.paimon.index.pk.PrimaryKeyIndexSourceFile;
import org.apache.paimon.index.pk.PrimaryKeyIndexSourcePolicy;
import org.apache.paimon.index.pksorted.PkSortedBucketIndexState;
import org.apache.paimon.index.pksorted.PkSortedIndexGroup;
import org.apache.paimon.io.DataFileMeta;
Expand Down Expand Up @@ -169,10 +170,15 @@ static Plan plan(
Pair<BinaryRow, Integer> bucket = bucketEntry.getKey();
List<IndexFileMeta> bucketPayloads =
payloadsByBucket.getOrDefault(bucket, Collections.emptyList());
Set<PrimaryKeyIndexSourceFile> activeSourceFiles = new HashSet<>();
Map<Integer, Set<PrimaryKeyIndexSourceFile>> activeSourceFilesByLevel = new HashMap<>();
for (DataFileMeta dataFile : bucketEntry.getValue()) {
activeSourceFiles.add(
new PrimaryKeyIndexSourceFile(dataFile.fileName(), dataFile.rowCount()));
if (PrimaryKeyIndexSourcePolicy.shouldRead(dataFile)) {
activeSourceFilesByLevel
.computeIfAbsent(dataFile.level(), ignored -> new HashSet<>())
.add(
new PrimaryKeyIndexSourceFile(
dataFile.fileName(), dataFile.rowCount()));
}
}
Map<String, Map<Integer, PkSortedIndexGroup>> groupsBySource = new LinkedHashMap<>();
for (PrimaryKeyIndexDefinition definition : scalarDefinitions) {
Expand All @@ -193,8 +199,11 @@ static Plan plan(
bucketEntry.getValue(),
definitionPayloads);
for (PkSortedIndexGroup group : state.groups()) {
Set<PrimaryKeyIndexSourceFile> activeGroupSources =
activeSourceFilesByLevel.getOrDefault(
group.dataLevel(), Collections.emptySet());
for (PrimaryKeyIndexSourceFile sourceFile : group.sourceFiles()) {
if (!activeSourceFiles.contains(sourceFile)) {
if (!activeGroupSources.contains(sourceFile)) {
continue;
}
groupsBySource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ void testAcceptsOnePayloadForCompleteLevel() {
}

@Test
void testRejectsPartialLevelPayload() {
void testAcceptsPayloadSubsetAndLeavesNewSourceUncovered() {
DataFileMeta first = dataFile("data-a", 3, 2);
DataFileMeta second = dataFile("data-b", 7, 2);
IndexFileMeta partial = payload("partial", 2, first);
Expand All @@ -73,9 +73,89 @@ void testRejectsPartialLevelPayload() {
Arrays.asList(first, second),
Collections.singletonList(partial));

assertThat(state.groups()).hasSize(1);
assertThat(state.coveredSourceFiles()).containsExactly(sourceFile(first));
assertThat(state.uncoveredSourceFiles()).containsExactly(sourceFile(second));
assertThat(state.rejectedPayloads()).isEmpty();
}

@Test
void testRetainsRetiredSourcesAndCoversOnlyActiveIntersection() {
DataFileMeta retired = dataFile("data-a", 3, 2);
DataFileMeta active = dataFile("data-b", 7, 2);
DataFileMeta newlyActive = dataFile("data-c", 5, 2);
IndexFileMeta payload = payload("index", 2, retired, active);

PkSortedBucketIndexState state =
PkSortedBucketIndexState.fromActiveDataFiles(
7,
"btree",
Arrays.asList(active, newlyActive),
Collections.singletonList(payload));

assertThat(state.groups()).hasSize(1);
assertThat(state.groups().get(0).sourceFiles())
.containsExactly(sourceFile(retired), sourceFile(active));
assertThat(state.coveredSourceFiles()).containsExactly(sourceFile(active));
assertThat(state.uncoveredSourceFiles()).containsExactly(sourceFile(newlyActive));
assertThat(state.rejectedPayloads()).isEmpty();
}

@Test
void testRejectsPayloadWithoutActiveSource() {
DataFileMeta retired = dataFile("data-a", 3, 2);
DataFileMeta active = dataFile("data-b", 7, 2);
IndexFileMeta payload = payload("index", 2, retired);

PkSortedBucketIndexState state =
PkSortedBucketIndexState.fromActiveDataFiles(
7,
"btree",
Collections.singletonList(active),
Collections.singletonList(payload));

assertThat(state.groups()).isEmpty();
assertThat(state.coveredSourceFiles()).isEmpty();
assertThat(state.uncoveredSourceFiles()).containsExactly(sourceFile(active));
assertThat(state.rejectedPayloads()).containsExactly(payload);
}

@Test
void testRejectsMismatchedActiveSourceRowCount() {
DataFileMeta active = dataFile("data", 3, 2);
DataFileMeta stale = dataFile("data", 4, 2);
IndexFileMeta payload = payload("index", 2, stale);

PkSortedBucketIndexState state =
PkSortedBucketIndexState.fromActiveDataFiles(
7,
"btree",
Collections.singletonList(active),
Collections.singletonList(payload));

assertThat(state.groups()).isEmpty();
assertThat(state.uncoveredSourceFiles()).hasSize(2);
assertThat(state.rejectedPayloads()).containsExactly(partial);
assertThat(state.uncoveredSourceFiles()).containsExactly(sourceFile(active));
assertThat(state.rejectedPayloads()).containsExactly(payload);
}

@Test
void testRejectsMisorderedPayloadSources() {
DataFileMeta first = dataFile("data-a", 3, 2);
DataFileMeta second = dataFile("data-b", 7, 2);
IndexFileMeta payload =
payload("index", 2, Arrays.asList(sourceFile(second), sourceFile(first)));

PkSortedBucketIndexState state =
PkSortedBucketIndexState.fromActiveDataFiles(
7,
"btree",
Arrays.asList(first, second),
Collections.singletonList(payload));

assertThat(state.groups()).isEmpty();
assertThat(state.uncoveredSourceFiles())
.containsExactly(sourceFile(first), sourceFile(second));
assertThat(state.rejectedPayloads()).containsExactly(payload);
}

@Test
Expand Down Expand Up @@ -159,6 +239,11 @@ private static IndexFileMeta payload(String name, int level, DataFileMeta... fil
new PrimaryKeyIndexSourceFile(
file.fileName(), file.rowCount()))
.collect(java.util.stream.Collectors.toList());
return payload(name, level, sources);
}

private static IndexFileMeta payload(
String name, int level, List<PrimaryKeyIndexSourceFile> sources) {
long rowCount = 0;
for (PrimaryKeyIndexSourceFile source : sources) {
rowCount += source.rowCount();
Expand All @@ -177,4 +262,8 @@ private static IndexFileMeta payload(String name, int level, DataFileMeta... fil
new PrimaryKeyIndexSourceMeta(level, sources).serialize()),
null);
}

private static PrimaryKeyIndexSourceFile sourceFile(DataFileMeta file) {
return new PrimaryKeyIndexSourceFile(file.fileName(), file.rowCount());
}
}
Loading
Loading