Skip to content

Commit 2684947

Browse files
committed
test: GH-0000 add exhaustive lmdb index selection combinatorics test
1 parent a3ad055 commit 2684947

1 file changed

Lines changed: 234 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
// Some portions generated by Codex
12+
package org.eclipse.rdf4j.sail.lmdb;
13+
14+
import static org.junit.Assert.assertEquals;
15+
import static org.junit.Assert.fail;
16+
17+
import java.io.File;
18+
import java.lang.reflect.Field;
19+
import java.util.ArrayList;
20+
import java.util.LinkedHashMap;
21+
import java.util.LinkedHashSet;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.function.Consumer;
26+
27+
import org.eclipse.rdf4j.sail.lmdb.config.LmdbStoreConfig;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.io.TempDir;
30+
31+
/**
32+
* Exhaustive combinatorics test for {@link TripleStore#getBestIndex(long, long, long, long)}.
33+
*/
34+
public class TripleStoreIndexSelectionCombinatoricsTest {
35+
36+
private static final int MAX_CONFIGURED_INDEXES = 6;
37+
38+
@Test
39+
public void selectsMaxScoreIndexForEveryPatternAndConfiguredIndexCombination(@TempDir File dataDir)
40+
throws Exception {
41+
List<String> allIndexSpecs = allIndexSpecs();
42+
Map<String, TripleStore.TripleIndex> detachedIndexes = createDetachedIndexes(dataDir, allIndexSpecs);
43+
List<long[]> statementPatterns = allStatementPatterns();
44+
45+
File selectorDir = new File(dataDir, "selector");
46+
selectorDir.mkdirs();
47+
48+
try (TripleStore selectorStore = new TripleStore(selectorDir, new LmdbStoreConfig("spoc"), null)) {
49+
List<TripleStore.TripleIndex> selectorIndexes = indexes(selectorStore);
50+
List<TripleStore.TripleIndex> originalSelectorIndexes = new ArrayList<>(selectorIndexes);
51+
long[] configuredCombinationCount = new long[1];
52+
long[] checkedSelections = new long[1];
53+
54+
try {
55+
forEachConfiguredIndexCombination(allIndexSpecs, configuredIndexSpecs -> {
56+
configuredCombinationCount[0]++;
57+
selectorIndexes.clear();
58+
for (String indexSpec : configuredIndexSpecs) {
59+
selectorIndexes.add(detachedIndexes.get(indexSpec));
60+
}
61+
62+
for (long[] pattern : statementPatterns) {
63+
TripleStore.TripleIndex selected = selectorStore.getBestIndex(pattern[0], pattern[1], pattern[2],
64+
pattern[3]);
65+
String selectedSpec = new String(selected.getFieldSeq());
66+
int selectedScore = patternScore(selectedSpec, pattern);
67+
68+
int bestScore = Integer.MIN_VALUE;
69+
Set<String> bestScoringSpecs = new LinkedHashSet<>();
70+
for (String configuredIndexSpec : configuredIndexSpecs) {
71+
int score = patternScore(configuredIndexSpec, pattern);
72+
if (score > bestScore) {
73+
bestScore = score;
74+
bestScoringSpecs.clear();
75+
bestScoringSpecs.add(configuredIndexSpec);
76+
} else if (score == bestScore) {
77+
bestScoringSpecs.add(configuredIndexSpec);
78+
}
79+
}
80+
81+
if (selectedScore != bestScore || !bestScoringSpecs.contains(selectedSpec)) {
82+
fail("Incorrect index selection for pattern " + patternLabel(pattern)
83+
+ " and configured indexes " + configuredIndexSpecs + ": selected=" + selectedSpec
84+
+ " (score=" + selectedScore + "), best=" + bestScoringSpecs + " (score="
85+
+ bestScore + ")");
86+
}
87+
88+
checkedSelections[0]++;
89+
}
90+
});
91+
} finally {
92+
selectorIndexes.clear();
93+
selectorIndexes.addAll(originalSelectorIndexes);
94+
}
95+
96+
assertEquals(expectedCombinationCount(allIndexSpecs.size(), MAX_CONFIGURED_INDEXES),
97+
configuredCombinationCount[0]);
98+
assertEquals(configuredCombinationCount[0] * statementPatterns.size(), checkedSelections[0]);
99+
}
100+
}
101+
102+
private static Map<String, TripleStore.TripleIndex> createDetachedIndexes(File dataDir, List<String> indexSpecs)
103+
throws Exception {
104+
Map<String, TripleStore.TripleIndex> indexes = new LinkedHashMap<>();
105+
for (String indexSpec : indexSpecs) {
106+
File donorDir = new File(dataDir, "donor-" + indexSpec);
107+
try (TripleStore donorStore = new TripleStore(donorDir, new LmdbStoreConfig(indexSpec), null)) {
108+
indexes.put(indexSpec, donorStore.getBestIndex(-1, -1, -1, -1));
109+
}
110+
}
111+
return indexes;
112+
}
113+
114+
private static List<long[]> allStatementPatterns() {
115+
List<long[]> patterns = new ArrayList<>(16);
116+
for (int mask = 0; mask < 16; mask++) {
117+
long subj = (mask & 0b0001) == 0 ? -1 : 11;
118+
long pred = (mask & 0b0010) == 0 ? -1 : 13;
119+
long obj = (mask & 0b0100) == 0 ? -1 : 17;
120+
long context = (mask & 0b1000) == 0 ? -1 : 0;
121+
patterns.add(new long[] { subj, pred, obj, context });
122+
}
123+
return patterns;
124+
}
125+
126+
private static String patternLabel(long[] pattern) {
127+
return "(" + componentLabel("s", pattern[0]) + ", " + componentLabel("p", pattern[1]) + ", "
128+
+ componentLabel("o", pattern[2]) + ", " + componentLabel("c", pattern[3]) + ")";
129+
}
130+
131+
private static String componentLabel(String name, long value) {
132+
return value >= 0 ? name + "=" + value : name + "=*";
133+
}
134+
135+
private static int patternScore(String indexSpec, long[] pattern) {
136+
int score = 0;
137+
for (int i = 0; i < indexSpec.length(); i++) {
138+
if (isBound(pattern, indexSpec.charAt(i))) {
139+
score++;
140+
} else {
141+
return score;
142+
}
143+
}
144+
return score;
145+
}
146+
147+
private static boolean isBound(long[] pattern, char component) {
148+
switch (component) {
149+
case 's':
150+
return pattern[0] >= 0;
151+
case 'p':
152+
return pattern[1] >= 0;
153+
case 'o':
154+
return pattern[2] >= 0;
155+
case 'c':
156+
return pattern[3] >= 0;
157+
default:
158+
throw new IllegalArgumentException("Unknown component: " + component);
159+
}
160+
}
161+
162+
private static List<String> allIndexSpecs() {
163+
List<String> indexSpecs = new ArrayList<>(24);
164+
char[] components = new char[] { 's', 'p', 'o', 'c' };
165+
permute(components, 0, indexSpecs);
166+
return indexSpecs;
167+
}
168+
169+
private static void permute(char[] components, int index, List<String> indexSpecs) {
170+
if (index == components.length) {
171+
indexSpecs.add(new String(components));
172+
return;
173+
}
174+
175+
for (int i = index; i < components.length; i++) {
176+
swap(components, index, i);
177+
permute(components, index + 1, indexSpecs);
178+
swap(components, index, i);
179+
}
180+
}
181+
182+
private static void swap(char[] values, int left, int right) {
183+
char current = values[left];
184+
values[left] = values[right];
185+
values[right] = current;
186+
}
187+
188+
private static void forEachConfiguredIndexCombination(List<String> allIndexSpecs,
189+
Consumer<List<String>> consumer) {
190+
int maxSize = Math.min(MAX_CONFIGURED_INDEXES, allIndexSpecs.size());
191+
List<String> current = new ArrayList<>(maxSize);
192+
for (int combinationSize = 1; combinationSize <= maxSize; combinationSize++) {
193+
buildCombinations(allIndexSpecs, 0, combinationSize, current, consumer);
194+
}
195+
}
196+
197+
private static void buildCombinations(List<String> values, int start, int remaining, List<String> current,
198+
Consumer<List<String>> consumer) {
199+
if (remaining == 0) {
200+
consumer.accept(new ArrayList<>(current));
201+
return;
202+
}
203+
204+
for (int i = start; i <= values.size() - remaining; i++) {
205+
current.add(values.get(i));
206+
buildCombinations(values, i + 1, remaining - 1, current, consumer);
207+
current.remove(current.size() - 1);
208+
}
209+
}
210+
211+
private static long expectedCombinationCount(int totalIndexes, int maxConfiguredIndexes) {
212+
int maxSize = Math.min(totalIndexes, maxConfiguredIndexes);
213+
long count = 0;
214+
for (int size = 1; size <= maxSize; size++) {
215+
count += nChooseK(totalIndexes, size);
216+
}
217+
return count;
218+
}
219+
220+
private static long nChooseK(int n, int k) {
221+
long value = 1;
222+
for (int i = 1; i <= k; i++) {
223+
value = value * (n - k + i) / i;
224+
}
225+
return value;
226+
}
227+
228+
@SuppressWarnings("unchecked")
229+
private static List<TripleStore.TripleIndex> indexes(TripleStore tripleStore) throws Exception {
230+
Field field = TripleStore.class.getDeclaredField("indexes");
231+
field.setAccessible(true);
232+
return (List<TripleStore.TripleIndex>) field.get(tripleStore);
233+
}
234+
}

0 commit comments

Comments
 (0)