Skip to content

Commit 13c624a

Browse files
authored
skip generating rank index match candidate for SQL (#3748)
Skip generating rank index match candidate for SQL, but keep the value index match candidate.
1 parent 0f2a55b commit 13c624a

File tree

2 files changed

+200
-1
lines changed

2 files changed

+200
-1
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/query/plan/cascades/MetaDataPlanContext.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.apple.foundationdb.record.RecordMetaData;
2525
import com.apple.foundationdb.record.RecordStoreState;
2626
import com.apple.foundationdb.record.metadata.Index;
27+
import com.apple.foundationdb.record.metadata.IndexTypes;
2728
import com.apple.foundationdb.record.metadata.RecordType;
2829
import com.apple.foundationdb.record.metadata.expressions.KeyExpression;
2930
import com.apple.foundationdb.record.provider.foundationdb.IndexMatchCandidateRegistry;
@@ -199,7 +200,14 @@ public static PlanContext forRootReference(@Nonnull final RecordQueryPlannerConf
199200

200201
final ImmutableSet.Builder<MatchCandidate> matchCandidatesBuilder = ImmutableSet.builder();
201202
for (final var index : indexList) {
202-
matchCandidatesBuilder.addAll(matchCandidateRegistry.createMatchCandidates(metaData, index, false));
203+
if (IndexTypes.RANK.equals(index.getType())) {
204+
final IndexExpansionInfo info = IndexExpansionInfo.createInfo(metaData, index, false);
205+
// For rank() we still want to create the match candidate for BY_VALUE scans.
206+
MatchCandidateExpansion.expandValueIndexMatchCandidate(info)
207+
.ifPresent(matchCandidatesBuilder::add);
208+
} else {
209+
matchCandidatesBuilder.addAll(matchCandidateRegistry.createMatchCandidates(metaData, index, false));
210+
}
203211
}
204212

205213
for (final var recordType : queriedRecordTypes) {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* MetaDataPlanContextRankIndexTest.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.foundationdb.record.query.plan.cascades;
22+
23+
import com.apple.foundationdb.record.RecordMetaData;
24+
import com.apple.foundationdb.record.RecordMetaDataBuilder;
25+
import com.apple.foundationdb.record.RecordStoreState;
26+
import com.apple.foundationdb.record.TestRecordsRankProto;
27+
import com.apple.foundationdb.record.metadata.Index;
28+
import com.apple.foundationdb.record.metadata.IndexTypes;
29+
import com.apple.foundationdb.record.provider.foundationdb.FDBRecordContext;
30+
import com.apple.foundationdb.record.provider.foundationdb.IndexMaintainerFactoryRegistryImpl;
31+
import com.apple.foundationdb.record.provider.foundationdb.query.FDBRecordStoreQueryTestBase;
32+
import com.apple.foundationdb.record.query.IndexQueryabilityFilter;
33+
import com.apple.foundationdb.record.query.RecordQuery;
34+
import com.apple.foundationdb.record.query.plan.RecordQueryPlannerConfiguration;
35+
import com.apple.foundationdb.record.query.plan.cascades.expressions.FullUnorderedScanExpression;
36+
import com.apple.foundationdb.record.query.plan.cascades.typing.Type;
37+
import com.apple.test.Tags;
38+
import com.google.common.collect.ImmutableSet;
39+
import org.junit.jupiter.api.Tag;
40+
import org.junit.jupiter.api.Test;
41+
42+
import java.util.Optional;
43+
import java.util.Set;
44+
import java.util.stream.Collectors;
45+
46+
import static com.apple.foundationdb.record.metadata.Key.Expressions.field;
47+
import static org.junit.jupiter.api.Assertions.assertEquals;
48+
import static org.junit.jupiter.api.Assertions.assertFalse;
49+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
50+
51+
/**
52+
* Tests for {@link MetaDataPlanContext} methods with rank indexes.
53+
* <ul>
54+
* <li>{@link MetaDataPlanContext#forRootReference} - verifies rank indexes only create
55+
* match candidates for value scans (BY_VALUE) and NOT for rank scans (BY_RANK)</li>
56+
* <li>{@link MetaDataPlanContext#forRecordQuery} - verifies rank indexes create both
57+
* value scan candidates (BY_VALUE) and windowed scan candidates (BY_RANK)</li>
58+
* </ul>
59+
*/
60+
@Tag(Tags.RequiresFDB)
61+
class MetaDataPlanContextRankIndexTest extends FDBRecordStoreQueryTestBase {
62+
63+
private RecordMetaData setupMetaDataWithRankIndex() {
64+
RecordMetaDataBuilder metaDataBuilder = RecordMetaData.newBuilder()
65+
.setRecords(TestRecordsRankProto.getDescriptor());
66+
67+
// Set primary key for HeaderRankedRecord (required by the proto definition)
68+
metaDataBuilder.getRecordType("HeaderRankedRecord")
69+
.setPrimaryKey(field("header").nest(field("group"), field("id")));
70+
71+
// Add a rank index on BasicRankedRecord
72+
metaDataBuilder.addIndex("BasicRankedRecord",
73+
new Index("rank_by_gender",
74+
field("score").groupBy(field("gender")),
75+
IndexTypes.RANK));
76+
77+
// Add another rank index with different structure
78+
metaDataBuilder.addIndex("BasicRankedRecord",
79+
new Index("simple_rank_score",
80+
field("score").ungrouped(),
81+
IndexTypes.RANK));
82+
83+
return metaDataBuilder.getRecordMetaData();
84+
}
85+
86+
@Test
87+
void testForRootReferenceRankIndexOnlyCreatesValueScanCandidate() {
88+
try (FDBRecordContext context = openContext()) {
89+
final RecordMetaData metaData = setupMetaDataWithRankIndex();
90+
91+
// Create and open a record store to get the state
92+
createOrOpenRecordStore(context, metaData);
93+
final RecordStoreState recordStoreState = recordStore.getRecordStoreState();
94+
95+
// Create a root reference for BasicRankedRecord
96+
final FullUnorderedScanExpression scanExpression = new FullUnorderedScanExpression(
97+
ImmutableSet.of(metaData.getRecordType("BasicRankedRecord").getName()),
98+
Type.Record.fromDescriptor(metaData.getRecordType("BasicRankedRecord").getDescriptor()),
99+
new AccessHints());
100+
101+
final Reference rootReference = Reference.initialOf(scanExpression);
102+
103+
// Create plan context using forRootReference
104+
final PlanContext planContext = MetaDataPlanContext.forRootReference(
105+
RecordQueryPlannerConfiguration.defaultPlannerConfiguration(),
106+
metaData,
107+
recordStoreState,
108+
IndexMaintainerFactoryRegistryImpl.instance(),
109+
rootReference,
110+
Optional.empty(),
111+
IndexQueryabilityFilter.DEFAULT);
112+
113+
// Get the match candidates
114+
final Set<MatchCandidate> matchCandidates = planContext.getMatchCandidates();
115+
// 3 rank index, each generates a ValueIndexScanMatchCandidate, and primary key generates a match candidate
116+
assertEquals(4, matchCandidates.size());
117+
118+
// Filter to only ValueIndexScanMatchCandidate with our rank index names
119+
final Set<MatchCandidate> rankIndexCandidates = matchCandidates.stream()
120+
.filter(candidate -> candidate.getName().equals("rank_by_gender") ||
121+
candidate.getName().equals("simple_rank_score") ||
122+
candidate.getName().equals("BasicRankedRecord$score"))
123+
.collect(Collectors.toSet());
124+
125+
// Verify that we have rank index candidates
126+
assertFalse(rankIndexCandidates.isEmpty(), "Should have rank index candidates");
127+
assertEquals(3, rankIndexCandidates.size(), "Should have exactly 2 rank index candidates");
128+
129+
// Verify all are ValueIndexScanMatchCandidate (for BY_VALUE scans)
130+
for (MatchCandidate candidate : rankIndexCandidates) {
131+
assertInstanceOf(ValueIndexScanMatchCandidate.class, candidate, "Rank index candidate should be a ValueIndexScanMatchCandidate, got: " + candidate.getClass().getName());
132+
}
133+
134+
// Verify no WindowedIndexScanMatchCandidate (BY_RANK scans) are created for rank indexes
135+
long windowedCandidateCount = matchCandidates.stream()
136+
.filter(candidate -> candidate instanceof WindowedIndexScanMatchCandidate)
137+
.count();
138+
139+
assertEquals(0, windowedCandidateCount,
140+
"Should not have any WindowedIndexScanMatchCandidate");
141+
}
142+
}
143+
144+
@Test
145+
void testForRecordQueryRankIndexCreatesWindowedScanCandidate() {
146+
try (FDBRecordContext context = openContext()) {
147+
final RecordMetaData metaData = setupMetaDataWithRankIndex();
148+
149+
// Create and open a record store to get the state
150+
createOrOpenRecordStore(context, metaData);
151+
final RecordStoreState recordStoreState = recordStore.getRecordStoreState();
152+
153+
// Create a simple RecordQuery for BasicRankedRecord
154+
final RecordQuery query = RecordQuery.newBuilder()
155+
.setRecordType("BasicRankedRecord")
156+
.build();
157+
158+
// Create plan context using forRecordQuery
159+
final PlanContext planContext = MetaDataPlanContext.forRecordQuery(
160+
RecordQueryPlannerConfiguration.defaultPlannerConfiguration(),
161+
metaData,
162+
recordStoreState,
163+
IndexMaintainerFactoryRegistryImpl.instance(),
164+
query);
165+
166+
// Get the match candidates
167+
final Set<MatchCandidate> matchCandidates = planContext.getMatchCandidates();
168+
assertEquals(7, matchCandidates.size());
169+
170+
// Filter to ValueIndexScanMatchCandidate with our rank index names
171+
final Set<String> valueIndexCandidateNames = matchCandidates.stream()
172+
.filter(candidate -> candidate instanceof ValueIndexScanMatchCandidate)
173+
.map(MatchCandidate::getName)
174+
.collect(Collectors.toSet());
175+
176+
// Verify that we have ValueIndexScanMatchCandidate for rank indexes
177+
assertEquals(ImmutableSet.of("rank_by_gender", "simple_rank_score", "BasicRankedRecord$score"), valueIndexCandidateNames,
178+
"Should have ValueIndexScanMatchCandidate for both rank indexes");
179+
180+
// Filter to WindowedIndexScanMatchCandidate with our rank index names
181+
final Set<String> windowedIndexCandidateNames = matchCandidates.stream()
182+
.filter(candidate -> candidate instanceof WindowedIndexScanMatchCandidate)
183+
.map(MatchCandidate::getName)
184+
.collect(Collectors.toSet());
185+
186+
// Verify that we have WindowedIndexScanMatchCandidate for rank indexes (BY_RANK scans)
187+
assertEquals(ImmutableSet.of("rank_by_gender", "simple_rank_score", "BasicRankedRecord$score"), windowedIndexCandidateNames,
188+
"Should have WindowedIndexScanMatchCandidate for both rank indexes");
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)