Skip to content

Commit 5260244

Browse files
authored
GH-5123 Ensure NativeStore remains usable after RDF*-star rejection (#5425)
2 parents ad20b90 + f6a91d4 commit 5260244

5 files changed

Lines changed: 260 additions & 11 deletions

File tree

core/sail/base/src/main/java/org/eclipse/rdf4j/sail/base/SailSourceBranch.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.eclipse.rdf4j.model.Model;
2828
import org.eclipse.rdf4j.model.ModelFactory;
2929
import org.eclipse.rdf4j.model.Resource;
30+
import org.eclipse.rdf4j.model.Statement;
3031
import org.eclipse.rdf4j.model.Value;
3132
import org.eclipse.rdf4j.model.impl.DynamicModelFactory;
3233
import org.eclipse.rdf4j.sail.SailException;
@@ -70,7 +71,7 @@ class SailSourceBranch implements SailSource {
7071

7172
/**
7273
* The {@link Model} instances that should be used to store {@link SailSink#approve(Resource, IRI, Value, Resource)}
73-
* and {@link SailSink#deprecate(Resource, IRI, Value, Resource)} statements.
74+
* and {@link SailSink#deprecate(Statement)} statements.
7475
*/
7576
private final ModelFactory modelFactory;
7677

@@ -306,7 +307,7 @@ public void flush() throws SailException {
306307
prepared = null;
307308
}
308309
}
309-
} catch (SailException e) {
310+
} catch (Throwable e) {
310311
// clear changes if flush fails
311312
changes.clear();
312313
prepared = null;

core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeSailStore.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.eclipse.rdf4j.model.Value;
3838
import org.eclipse.rdf4j.model.ValueFactory;
3939
import org.eclipse.rdf4j.query.algebra.evaluation.impl.EvaluationStatistics;
40+
import org.eclipse.rdf4j.sail.Sail;
4041
import org.eclipse.rdf4j.sail.SailException;
4142
import org.eclipse.rdf4j.sail.base.BackingSailSource;
4243
import org.eclipse.rdf4j.sail.base.Changeset;
@@ -389,7 +390,11 @@ public synchronized void flush() throws SailException {
389390
throw new SailException(e);
390391
} catch (RuntimeException e) {
391392
logger.error("Encountered an unexpected problem while trying to commit", e);
392-
throw e;
393+
if (e instanceof SailException) {
394+
throw e;
395+
}
396+
// Ensure upstream handles this as a SailException so branch flush clears pending changes
397+
throw new SailException(e);
393398
} finally {
394399
sinkStoreAccessLock.unlock();
395400
}
@@ -445,7 +450,15 @@ public void clear(Resource... contexts) throws SailException {
445450

446451
@Override
447452
public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException {
448-
addStatement(subj, pred, obj, explicit, ctx);
453+
try {
454+
addStatement(subj, pred, obj, explicit, ctx);
455+
} catch (RuntimeException e) {
456+
if (e instanceof SailException) {
457+
throw e;
458+
}
459+
// Ensure upstream handles this as a SailException so branch flush clears pending changes
460+
throw new SailException(e);
461+
}
449462
}
450463

451464
@Override
@@ -478,8 +491,11 @@ public void approveAll(Set<Statement> approved, Set<Resource> approvedContexts)
478491
} catch (IOException e) {
479492
throw new SailException(e);
480493
} catch (RuntimeException e) {
494+
if (e instanceof SailException) {
495+
throw e;
496+
}
481497
logger.error("Encountered an unexpected problem while trying to add a statement", e);
482-
throw e;
498+
throw new SailException(e);
483499
} finally {
484500
sinkStoreAccessLock.unlock();
485501
}
@@ -540,8 +556,11 @@ private boolean addStatement(Resource subj, IRI pred, Value obj, boolean explici
540556
} catch (IOException e) {
541557
throw new SailException(e);
542558
} catch (RuntimeException e) {
559+
if (e instanceof SailException) {
560+
throw e;
561+
}
543562
logger.error("Encountered an unexpected problem while trying to add a statement", e);
544-
throw e;
563+
throw new SailException(e);
545564
} finally {
546565
sinkStoreAccessLock.unlock();
547566
}
@@ -614,7 +633,11 @@ private long removeStatements(Resource subj, IRI pred, Value obj, boolean explic
614633
throw new SailException(e);
615634
} catch (RuntimeException e) {
616635
logger.error("Encountered an unexpected problem while trying to remove statements", e);
617-
throw e;
636+
if (e instanceof SailException) {
637+
throw e;
638+
}
639+
// Ensure upstream handles this as a SailException so branch flush clears pending changes
640+
throw new SailException(e);
618641
} finally {
619642
sinkStoreAccessLock.unlock();
620643
}

core/sail/nativerdf/src/main/java/org/eclipse/rdf4j/sail/nativerdf/NativeStoreConnection.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ protected void commitInternal() throws SailException {
7676
try {
7777
super.commitInternal();
7878
} finally {
79-
txnLock.release();
80-
txnLock = null;
79+
if (txnLock != null) {
80+
txnLock.release();
81+
txnLock = null;
82+
}
8183
}
8284

8385
nativeStore.notifySailChanged(sailChangedEvent);
@@ -91,8 +93,10 @@ protected void rollbackInternal() throws SailException {
9193
try {
9294
super.rollbackInternal();
9395
} finally {
94-
txnLock.release();
95-
txnLock = null;
96+
if (txnLock != null) {
97+
txnLock.release();
98+
txnLock = null;
99+
}
96100
}
97101
// create a fresh event object.
98102
sailChangedEvent = new DefaultSailChangedEvent(nativeStore);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (c) 2025 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+
package org.eclipse.rdf4j.sail.nativerdf;
12+
13+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
14+
15+
import java.io.File;
16+
import java.lang.reflect.Field;
17+
import java.nio.file.Files;
18+
import java.util.Map;
19+
20+
import org.eclipse.rdf4j.model.IRI;
21+
import org.eclipse.rdf4j.model.Resource;
22+
import org.eclipse.rdf4j.model.ValueFactory;
23+
import org.eclipse.rdf4j.sail.SailException;
24+
import org.eclipse.rdf4j.sail.base.SailSink;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
/**
30+
* Verifies that NativeSailStore wraps unexpected RuntimeExceptions in SailException so upstream callers can reliably
31+
* handle failures (e.g., branch flush clearing pending changes).
32+
*/
33+
public class NativeSailStoreRuntimeWrappingIT {
34+
35+
private File dataDir;
36+
private NativeSailStore store;
37+
38+
@BeforeEach
39+
public void setUp() throws Exception {
40+
dataDir = Files.createTempDirectory("nativestore-wrap-test").toFile();
41+
store = new NativeSailStore(dataDir, "spoc");
42+
}
43+
44+
@AfterEach
45+
public void tearDown() throws Exception {
46+
if (store != null) {
47+
store.close();
48+
}
49+
}
50+
51+
@Test
52+
public void testFlushWrapsRuntimeIntoSailException() throws Exception {
53+
// Replace TripleStore with a stub that throws at commit()
54+
replaceTripleStore(new ThrowOnCommitTripleStore(dataDir, "spoc"));
55+
56+
// Add a statement to ensure a triplestore transaction is started
57+
SailSink sink = store.getExplicitSailSource().sink(null);
58+
ValueFactory vf = store.getValueFactory();
59+
IRI s = vf.createIRI("urn:s");
60+
IRI p = vf.createIRI("urn:p");
61+
IRI o = vf.createIRI("urn:o");
62+
sink.approve(s, p, o, (Resource) null);
63+
64+
// Now flush, expecting a SailException (wrapping the runtime)
65+
assertThatThrownBy(() -> sink.flush())
66+
.isInstanceOf(SailException.class);
67+
}
68+
69+
@Test
70+
public void testRemoveStatementsWrapsRuntimeIntoSailException() throws Exception {
71+
// Replace TripleStore with a stub that throws at removeTriplesByContext()
72+
replaceTripleStore(new ThrowOnRemoveTripleStore(dataDir, "spoc"));
73+
74+
SailSink sink = store.getExplicitSailSource().sink(null);
75+
// Expect SailException when attempting to remove (deprecateByQuery delegates)
76+
assertThatThrownBy(() -> sink.deprecateByQuery(null, null, null, new Resource[] { null }))
77+
.isInstanceOf(SailException.class);
78+
}
79+
80+
private void replaceTripleStore(TripleStore newTripleStore) throws Exception {
81+
Field f = NativeSailStore.class.getDeclaredField("tripleStore");
82+
f.setAccessible(true);
83+
f.set(store, newTripleStore);
84+
}
85+
86+
private static class ThrowOnCommitTripleStore extends TripleStore {
87+
public ThrowOnCommitTripleStore(File dir, String indexSpecStr) throws Exception {
88+
super(dir, indexSpecStr, false);
89+
}
90+
91+
@Override
92+
public void commit() {
93+
throw new RuntimeException("simulated failure during commit");
94+
}
95+
}
96+
97+
private static class ThrowOnRemoveTripleStore extends TripleStore {
98+
public ThrowOnRemoveTripleStore(File dir, String indexSpecStr) throws Exception {
99+
super(dir, indexSpecStr, false);
100+
}
101+
102+
@Override
103+
public Map<Integer, Long> removeTriplesByContext(int subjID, int predID, int objID, int contextId,
104+
boolean explicit) {
105+
throw new RuntimeException("simulated failure during removeTriplesByContext");
106+
}
107+
108+
@Override
109+
public void startTransaction() {
110+
// no-op; we're only interested in remove path throwing
111+
}
112+
}
113+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 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+
package org.eclipse.rdf4j.sail.nativerdf;
12+
13+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
14+
15+
import java.io.ByteArrayInputStream;
16+
import java.io.File;
17+
import java.nio.charset.StandardCharsets;
18+
19+
import org.eclipse.rdf4j.common.transaction.IsolationLevel;
20+
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
21+
import org.eclipse.rdf4j.model.Statement;
22+
import org.eclipse.rdf4j.repository.Repository;
23+
import org.eclipse.rdf4j.repository.RepositoryConnection;
24+
import org.eclipse.rdf4j.repository.RepositoryResult;
25+
import org.eclipse.rdf4j.repository.sail.SailRepository;
26+
import org.eclipse.rdf4j.rio.RDFFormat;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.io.TempDir;
29+
30+
/**
31+
* Reproduces a reported issue where attempting to add Turtle-star data to a NativeStore throws, and subsequently the
32+
* repository becomes unusable for normal operations. After the rejection, the repository should remain usable.
33+
*/
34+
public class NativeStoreRDFStarRejectionTest {
35+
36+
@TempDir
37+
public File dataDir;
38+
39+
@Test
40+
public void nativeStoreRejectsTurtleStarButRemainsUsable() {
41+
String data = "@prefix ex: <http://example.org/> .\n" +
42+
"@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n" +
43+
"\n" +
44+
"# Basic triple\n" +
45+
"ex:JohnDoe ex:worksAt ex:CompanyX .\n" +
46+
"\n" +
47+
"# RDF* triple (unsupported by NativeStore)\n" +
48+
"<<ex:JohnDoe ex:worksAt ex:CompanyX>> ex:since \"2022-01-01\"^^xsd:date .\n";
49+
50+
Repository repo = new SailRepository(new NativeStore(dataDir));
51+
52+
// First: attempt to add data that includes RDF*-star. Expect an exception (rejection).
53+
try (RepositoryConnection conn = repo.getConnection()) {
54+
try {
55+
conn.add(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), null, RDFFormat.TURTLE);
56+
} catch (Exception expected) {
57+
// Expected: Turtle-star should be rejected by NativeStore
58+
}
59+
}
60+
61+
// Then: repository should still be usable. Getting statements must not throw.
62+
assertDoesNotThrow(() -> {
63+
try (RepositoryConnection conn = repo.getConnection();
64+
RepositoryResult<Statement> result = conn.getStatements(null, null, null)) {
65+
// iterate to fully exercise the result set
66+
while (result.hasNext()) {
67+
result.next();
68+
}
69+
}
70+
}, "Repository became unusable after rejecting Turtle-star input");
71+
}
72+
73+
@Test
74+
public void nativeStoreRejectsTurtleStarButRemainsUsableSnapshot() {
75+
String data = "@prefix ex: <http://example.org/> .\n" +
76+
"@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n" +
77+
"\n" +
78+
"# Basic triple\n" +
79+
"ex:JohnDoe ex:worksAt ex:CompanyX .\n" +
80+
"\n" +
81+
"# RDF* triple (unsupported by NativeStore)\n" +
82+
"<<ex:JohnDoe ex:worksAt ex:CompanyX>> ex:since \"2022-01-01\"^^xsd:date .\n";
83+
84+
Repository repo = new SailRepository(new NativeStore(dataDir));
85+
86+
// First: attempt to add data that includes RDF*-star. Expect an exception (rejection).
87+
try (RepositoryConnection conn = repo.getConnection()) {
88+
try {
89+
conn.begin(IsolationLevels.SNAPSHOT);
90+
conn.add(new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), null, RDFFormat.TURTLE);
91+
conn.commit();
92+
} catch (Exception expected) {
93+
// Expected: Turtle-star should be rejected by NativeStore
94+
}
95+
}
96+
97+
// Then: repository should still be usable. Getting statements must not throw.
98+
assertDoesNotThrow(() -> {
99+
try (RepositoryConnection conn = repo.getConnection();
100+
RepositoryResult<Statement> result = conn.getStatements(null, null, null)) {
101+
// iterate to fully exercise the result set
102+
while (result.hasNext()) {
103+
result.next();
104+
}
105+
}
106+
}, "Repository became unusable after rejecting Turtle-star input");
107+
}
108+
}

0 commit comments

Comments
 (0)