Skip to content

Commit cec2263

Browse files
authored
Merge main into develop (#5429)
2 parents 798efc4 + 6a92f19 commit cec2263

8 files changed

Lines changed: 307 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ Do **not** modify existing headers’ years.
510510
511511
## Branch & PR Workflow (Agent)
512512
513+
- Confirm issue number first (mandatory): before creating a branch, pause and request/confirm the GitHub issue number. Do not proceed to branch creation until the issue number is provided or confirmed.
513514
- Name branch: `GH-<issue>-<short-slug>` (kebab‑case slug).
514515
- Create branch: `git checkout -b GH-XXXX-your-slug`.
515516
- Stage changes: `git add -A` (ensure new Java files have the required header).

core/rio/turtle/src/main/java/org/eclipse/rdf4j/rio/turtle/TurtleParser.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,11 @@ protected Literal parseNumber() throws IOException, RDFParseException {
808808
// read optional fractional digits
809809
if (c == '.') {
810810

811-
if (TurtleUtil.isWhitespace(peekCodePoint())) {
811+
int next = peekCodePoint();
812+
// Treat '.' as statement terminator at EOF only when we already parsed at least one digit
813+
// (e.g., "30.") or when whitespace follows. Otherwise, attempt to parse as decimal
814+
// which will surface a useful error for a stray '.' token.
815+
if ((value.length() > 0 && next == -1) || TurtleUtil.isWhitespace(next)) {
812816
// We're parsing an integer that did not have a space before
813817
// the
814818
// period to end the statement

core/rio/turtle/src/test/java/org/eclipse/rdf4j/rio/turtle/TurtleParserTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,47 @@ public void testParseDots() throws IOException {
8989

9090
}
9191

92+
@Test
93+
public void testIntegerFollowedByDotAtEOF() throws IOException {
94+
// Reproduces bug: integer literal immediately followed by statement terminator '.' at EOF
95+
String data = prefixes + ":alice :age 30."; // no trailing whitespace/newline
96+
97+
parser.parse(new StringReader(data), baseURI);
98+
99+
assertTrue(errorCollector.getWarnings().isEmpty());
100+
assertTrue(errorCollector.getErrors().isEmpty());
101+
assertTrue(errorCollector.getFatalErrors().isEmpty());
102+
103+
assertEquals(1, statementCollector.getStatements().size());
104+
Statement st = statementCollector.getStatements().iterator().next();
105+
assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject());
106+
assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate());
107+
assertTrue(st.getObject() instanceof Literal);
108+
Literal lit = (Literal) st.getObject();
109+
assertEquals("30", lit.getLabel());
110+
assertEquals(XSD.INTEGER, lit.getDatatype());
111+
}
112+
113+
@Test
114+
public void testLetterFollowedByDotAtEOF() throws IOException {
115+
String data = prefixes + ":alice :age \"a\"."; // no trailing whitespace/newline
116+
117+
parser.parse(new StringReader(data), baseURI);
118+
119+
assertTrue(errorCollector.getWarnings().isEmpty());
120+
assertTrue(errorCollector.getErrors().isEmpty());
121+
assertTrue(errorCollector.getFatalErrors().isEmpty());
122+
123+
assertEquals(1, statementCollector.getStatements().size());
124+
Statement st = statementCollector.getStatements().iterator().next();
125+
assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject());
126+
assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate());
127+
assertTrue(st.getObject() instanceof Literal);
128+
Literal lit = (Literal) st.getObject();
129+
assertEquals("a", lit.getLabel());
130+
assertEquals(XSD.STRING, lit.getDatatype());
131+
}
132+
92133
@Test
93134
public void testParseIllegalURIFatal() throws IOException {
94135
String data = " <urn:foo_bar\\r> <urn:foo> <urn:bar> ; <urn:foo2> <urn:bar2> . <urn:foobar> <urn:food> <urn:barf> . ";

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+
}

0 commit comments

Comments
 (0)