Skip to content

Commit 13db1a1

Browse files
committed
fix bugs
1 parent 47cb10b commit 13db1a1

5 files changed

Lines changed: 311 additions & 14 deletions

File tree

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbLftjExecutor.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,7 @@ private LmdbCompiledLftjFactory compiledFactory(LmdbQueryAccess queryAccess, Lmd
7373
String cacheKey = compiler.cacheKey(plan, shape, queryAccess.includeInferred());
7474
LmdbLftjCodegenCache.CacheEntry cached = queryAccess.cachedCompiledPlan(cacheKey);
7575
if (cached != null) {
76-
if (cached.compiled()) {
77-
return cached.factory();
78-
}
79-
throw codegenFailure(cacheKey, cached.failureMessage(), null);
76+
return cached.compiled() ? cached.factory() : null;
8077
}
8178

8279
try {
@@ -85,16 +82,8 @@ private LmdbCompiledLftjFactory compiledFactory(LmdbQueryAccess queryAccess, Lmd
8582
return factory;
8683
} catch (RuntimeException e) {
8784
queryAccess.cacheCompiledPlanFailure(cacheKey, e.getMessage());
88-
throw codegenFailure(cacheKey, e.getMessage(), e);
89-
}
90-
}
91-
92-
private IllegalStateException codegenFailure(String cacheKey, String message, RuntimeException cause) {
93-
String detail = message == null || message.isBlank() ? "<no detail>" : message;
94-
if (cause == null) {
95-
return new IllegalStateException("LMDB LFTJ codegen failed for " + cacheKey + ": " + detail);
85+
return null;
9686
}
97-
return new IllegalStateException("LMDB LFTJ codegen failed for " + cacheKey + ": " + detail, cause);
9887
}
9988

10089
private final class LmdbLftjIteration extends LookAheadIteration<BindingSet> {

core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbLftjOptimizer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ private PlanningTarget tryExtractPlanningTarget(Join node, List<TupleExpr> opera
217217
return null;
218218
}
219219

220-
boolean preserveOuterOperators = filterPartition.filterRewrites()
220+
boolean preserveOuterOperators = !rootReplaceable || filterPartition.filterRewrites()
221221
.stream()
222222
.anyMatch(filterRewrite -> filterRewrite.residualCondition() != null);
223223
List<LmdbLftjPlan.OutputBinding> outputBindings = collectOutputBindings(projection, extension, visibleVariables,

core/sail/lmdb/src/test/java/org/eclipse/rdf4j/sail/lmdb/LmdbLftjExecutorTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,73 @@ void evaluateShouldAvoidRecordScansForHiddenContextMultiplicity() {
166166
assertEquals(0, queryAccess.recordScanCalls,
167167
"hidden context multiplicity should come from cached frontier counts, not RecordIterator rescans");
168168
}
169+
170+
@Test
171+
void evaluateShouldFallbackToInterpretedPathWhenCodegenCompilationFails() {
172+
FailingCompiler compiler = new FailingCompiler();
173+
FailingCachingQueryAccess queryAccess = new FailingCachingQueryAccess(compiler);
174+
QueryEvaluationStep evaluationStep = LmdbLftjSyntheticScenario.createEvaluationStep(queryAccess);
175+
176+
long firstPassCount = countRows(evaluationStep);
177+
long secondPassCount = countRows(evaluationStep);
178+
179+
assertTrue(firstPassCount > 0, "the interpreted executor should still enumerate results after codegen fails");
180+
assertEquals(firstPassCount, secondPassCount,
181+
"cached codegen failures should keep using the interpreted executor on later evaluations");
182+
assertEquals(1, compiler.compileCalls,
183+
"codegen should fail once and then stay on the interpreted path for the same execution key");
184+
}
185+
186+
private long countRows(QueryEvaluationStep evaluationStep) {
187+
long count = 0;
188+
try (CloseableIteration<BindingSet> iteration = evaluationStep.evaluate(EmptyBindingSet.getInstance())) {
189+
while (iteration.hasNext()) {
190+
iteration.next();
191+
count++;
192+
}
193+
}
194+
return count;
195+
}
196+
197+
private static final class FailingCachingQueryAccess extends LmdbLftjSyntheticScenario.TestQueryAccess {
198+
199+
private final FailingCompiler compiler;
200+
private LmdbLftjCodegenCache.CacheEntry cachedEntry;
201+
202+
private FailingCachingQueryAccess(FailingCompiler compiler) {
203+
this.compiler = compiler;
204+
}
205+
206+
@Override
207+
public LmdbLftjCodegenCache.CacheEntry cachedCompiledPlan(String executionKey) {
208+
return cachedEntry;
209+
}
210+
211+
@Override
212+
public void cacheCompiledPlanSuccess(String executionKey, LmdbCompiledLftjFactory factory) {
213+
cachedEntry = LmdbLftjCodegenCache.CacheEntry.success(factory);
214+
}
215+
216+
@Override
217+
public void cacheCompiledPlanFailure(String executionKey, String message) {
218+
cachedEntry = LmdbLftjCodegenCache.CacheEntry.failure(message);
219+
}
220+
221+
@Override
222+
public LmdbLftjCodegenCompiler codegenCompiler() {
223+
return compiler;
224+
}
225+
}
226+
227+
private static final class FailingCompiler extends LmdbLftjCodegenCompiler {
228+
229+
private int compileCalls;
230+
231+
@Override
232+
LmdbCompiledLftjFactory compile(LmdbLftjPlan plan, LmdbLftjExecutionShape shape, boolean includeInferred,
233+
LmdbQueryAccess queryAccess) {
234+
compileCalls++;
235+
throw new IllegalArgumentException("synthetic compile failure");
236+
}
237+
}
169238
}

core/sail/lmdb/src/test/java/org/eclipse/rdf4j/sail/lmdb/LmdbLftjFusionCorrectnessTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ void aliasedProjectionWithResidualFilterRowsShouldMatchRegularEvaluation(@TempDi
6868
assertRowsMatch(tempDir, aliasedProjectionWithResidualFilterQuery());
6969
}
7070

71+
@Test
72+
void orderedAliasedCycleRowsShouldMatchRegularEvaluation(@TempDir java.nio.file.Path tempDir) {
73+
assertRowsMatch(tempDir, orderedAliasedCycleQuery());
74+
}
75+
7176
@Test
7277
void aliasedCycleRowsShouldMatchRegularEvaluationWithBoundSourceVariables(@TempDir java.nio.file.Path tempDir) {
7378
assertEquals(List.of(
@@ -272,4 +277,15 @@ private String aliasedProjectionWithResidualFilterQuery() {
272277
+ " FILTER (?a != ?b && ?a != ?c && ?b != ?c && STRSTARTS(STR(?a), \"urn:person:1\"))\n"
273278
+ "}\n";
274279
}
280+
281+
private String orderedAliasedCycleQuery() {
282+
return "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
283+
+ "SELECT DISTINCT (?a AS ?x) (?b AS ?y) (?c AS ?z) WHERE {\n"
284+
+ " ?a foaf:knows ?b .\n"
285+
+ " ?b foaf:knows ?c .\n"
286+
+ " ?c foaf:knows ?a .\n"
287+
+ " FILTER (?a != ?b && ?a != ?c && ?b != ?c)\n"
288+
+ "}\n"
289+
+ "ORDER BY ?x ?y ?z\n";
290+
}
275291
}
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
import java.io.File;
18+
import java.util.Set;
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
21+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
22+
import org.eclipse.rdf4j.model.IRI;
23+
import org.eclipse.rdf4j.model.Value;
24+
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
25+
import org.eclipse.rdf4j.model.vocabulary.FOAF;
26+
import org.eclipse.rdf4j.query.TupleQueryResult;
27+
import org.eclipse.rdf4j.repository.Repository;
28+
import org.eclipse.rdf4j.repository.RepositoryConnection;
29+
import org.eclipse.rdf4j.repository.sail.SailRepository;
30+
import org.eclipse.rdf4j.sail.NotifyingSailConnection;
31+
import org.eclipse.rdf4j.sail.SailException;
32+
import org.eclipse.rdf4j.sail.lmdb.config.LmdbStoreConfig;
33+
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.api.io.TempDir;
35+
36+
class LmdbLftjSnapshotIsolationTest {
37+
38+
private static final SimpleValueFactory VF = SimpleValueFactory.getInstance();
39+
40+
@Test
41+
void queryShouldKeepDatasetSnapshotWhenLftjStartsReading(@TempDir File dataDir) {
42+
LmdbStoreConfig config = new LmdbStoreConfig("spoc,sopc,psoc,posc,ospc,opsc");
43+
config.setLftjEnabled(true);
44+
config.setLftjCodegenEnabled(false);
45+
config.setForceSync(false);
46+
47+
SnapshotHookStore store = new SnapshotHookStore(dataDir, config);
48+
Repository repository = new SailRepository(store);
49+
IRI a = VF.createIRI("urn:person:a");
50+
IRI b = VF.createIRI("urn:person:b");
51+
IRI c = VF.createIRI("urn:person:c");
52+
53+
repository.init();
54+
try {
55+
try (RepositoryConnection writer = repository.getConnection()) {
56+
writer.add(a, FOAF.KNOWS, b);
57+
writer.add(b, FOAF.KNOWS, c);
58+
}
59+
60+
AtomicBoolean injectedEdge = new AtomicBoolean();
61+
store.setBeforeAcquireReadTxn(() -> {
62+
if (injectedEdge.compareAndSet(false, true)) {
63+
try (RepositoryConnection writer = repository.getConnection()) {
64+
writer.add(c, FOAF.KNOWS, a);
65+
}
66+
}
67+
});
68+
69+
long rowCount = 0;
70+
try (RepositoryConnection reader = repository.getConnection()) {
71+
reader.begin(IsolationLevels.SNAPSHOT);
72+
try (TupleQueryResult result = reader.prepareTupleQuery(cycleQuery()).evaluate()) {
73+
while (result.hasNext()) {
74+
result.next();
75+
rowCount++;
76+
}
77+
}
78+
reader.commit();
79+
}
80+
81+
assertTrue(injectedEdge.get(), "sanity check: the third cycle edge must be committed during evaluation");
82+
assertEquals(0, rowCount,
83+
"LFTJ should stay on the pinned SailDataset snapshot instead of reading a newer LMDB transaction");
84+
} finally {
85+
repository.shutDown();
86+
}
87+
}
88+
89+
private String cycleQuery() {
90+
return "PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n"
91+
+ "SELECT * WHERE {\n"
92+
+ " ?a foaf:knows ?b .\n"
93+
+ " ?b foaf:knows ?c .\n"
94+
+ " ?c foaf:knows ?a .\n"
95+
+ " FILTER (?a != ?b && ?a != ?c && ?b != ?c)\n"
96+
+ "}\n";
97+
}
98+
99+
private static final class SnapshotHookStore extends LmdbStore {
100+
101+
private volatile Runnable beforeAcquireReadTxn = () -> {
102+
};
103+
104+
private SnapshotHookStore(File dataDir, LmdbStoreConfig config) {
105+
super(dataDir, config);
106+
}
107+
108+
private void setBeforeAcquireReadTxn(Runnable beforeAcquireReadTxn) {
109+
this.beforeAcquireReadTxn = beforeAcquireReadTxn;
110+
}
111+
112+
@Override
113+
protected NotifyingSailConnection getConnectionInternal() throws SailException {
114+
return new SnapshotHookConnection(this);
115+
}
116+
}
117+
118+
private static final class SnapshotHookConnection extends LmdbStoreConnection {
119+
120+
private final SnapshotHookStore store;
121+
122+
private SnapshotHookConnection(SnapshotHookStore store) {
123+
super(store);
124+
this.store = store;
125+
}
126+
127+
@Override
128+
protected LmdbQueryAccess createQueryAccess(boolean includeInferred) {
129+
LmdbQueryAccess delegate = super.createQueryAccess(includeInferred);
130+
AtomicBoolean fired = new AtomicBoolean();
131+
return new LmdbQueryAccess() {
132+
@Override
133+
public TripleStore tripleStore() {
134+
return delegate.tripleStore();
135+
}
136+
137+
@Override
138+
public TxnManager.Txn acquireReadTxn() {
139+
if (fired.compareAndSet(false, true)) {
140+
store.beforeAcquireReadTxn.run();
141+
}
142+
return delegate.acquireReadTxn();
143+
}
144+
145+
@Override
146+
public void releaseReadTxn(TxnManager.Txn txn) {
147+
delegate.releaseReadTxn(txn);
148+
}
149+
150+
@Override
151+
public long resolveId(Value value) {
152+
return delegate.resolveId(value);
153+
}
154+
155+
@Override
156+
public Value resolveValue(long id) {
157+
return delegate.resolveValue(id);
158+
}
159+
160+
@Override
161+
public Value lazyValue(long id) {
162+
return delegate.lazyValue(id);
163+
}
164+
165+
@Override
166+
public boolean includeInferred() {
167+
return delegate.includeInferred();
168+
}
169+
170+
@Override
171+
public Set<String> configuredIndexes() {
172+
return delegate.configuredIndexes();
173+
}
174+
175+
@Override
176+
public RecordIterator openScan(TxnManager.Txn txn, String indexName, long subj, long pred, long obj,
177+
long context, boolean explicit) {
178+
return delegate.openScan(txn, indexName, subj, pred, obj, context, explicit);
179+
}
180+
181+
@Override
182+
public LmdbTrieKeyCursor openTrieCursor(TxnManager.Txn txn, String indexName, boolean explicit) {
183+
return delegate.openTrieCursor(txn, indexName, explicit);
184+
}
185+
186+
@Override
187+
public LmdbLftjPlanner.PlanningResult cachedPlanningResult(String cacheKey) {
188+
return delegate.cachedPlanningResult(cacheKey);
189+
}
190+
191+
@Override
192+
public void cachePlanningResult(String cacheKey, LmdbLftjPlanner.PlanningResult result) {
193+
delegate.cachePlanningResult(cacheKey, result);
194+
}
195+
196+
@Override
197+
public boolean lftjCodegenEnabled() {
198+
return delegate.lftjCodegenEnabled();
199+
}
200+
201+
@Override
202+
public LmdbLftjCodegenCache.CacheEntry cachedCompiledPlan(String executionKey) {
203+
return delegate.cachedCompiledPlan(executionKey);
204+
}
205+
206+
@Override
207+
public void cacheCompiledPlanSuccess(String executionKey, LmdbCompiledLftjFactory factory) {
208+
delegate.cacheCompiledPlanSuccess(executionKey, factory);
209+
}
210+
211+
@Override
212+
public void cacheCompiledPlanFailure(String executionKey, String message) {
213+
delegate.cacheCompiledPlanFailure(executionKey, message);
214+
}
215+
216+
@Override
217+
public LmdbLftjCodegenCompiler codegenCompiler() {
218+
return delegate.codegenCompiler();
219+
}
220+
};
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)