Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ Do **not** modify existing headers’ years.

## Branch & PR Workflow (Agent)

- 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.
- Name branch: `GH-<issue>-<short-slug>` (kebab‑case slug).
- Create branch: `git checkout -b GH-XXXX-your-slug`.
- Stage changes: `git add -A` (ensure new Java files have the required header).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,11 @@ protected Literal parseNumber() throws IOException, RDFParseException {
// read optional fractional digits
if (c == '.') {

if (TurtleUtil.isWhitespace(peekCodePoint())) {
int next = peekCodePoint();
// Treat '.' as statement terminator at EOF only when we already parsed at least one digit
// (e.g., "30.") or when whitespace follows. Otherwise, attempt to parse as decimal
// which will surface a useful error for a stray '.' token.
if ((value.length() > 0 && next == -1) || TurtleUtil.isWhitespace(next)) {
// We're parsing an integer that did not have a space before
// the
// period to end the statement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,47 @@ public void testParseDots() throws IOException {

}

@Test
public void testIntegerFollowedByDotAtEOF() throws IOException {
// Reproduces bug: integer literal immediately followed by statement terminator '.' at EOF
String data = prefixes + ":alice :age 30."; // no trailing whitespace/newline

parser.parse(new StringReader(data), baseURI);

assertTrue(errorCollector.getWarnings().isEmpty());
assertTrue(errorCollector.getErrors().isEmpty());
assertTrue(errorCollector.getFatalErrors().isEmpty());

assertEquals(1, statementCollector.getStatements().size());
Statement st = statementCollector.getStatements().iterator().next();
assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject());
assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate());
assertTrue(st.getObject() instanceof Literal);
Literal lit = (Literal) st.getObject();
assertEquals("30", lit.getLabel());
assertEquals(XSD.INTEGER, lit.getDatatype());
}

@Test
public void testLetterFollowedByDotAtEOF() throws IOException {
String data = prefixes + ":alice :age \"a\"."; // no trailing whitespace/newline

parser.parse(new StringReader(data), baseURI);

assertTrue(errorCollector.getWarnings().isEmpty());
assertTrue(errorCollector.getErrors().isEmpty());
assertTrue(errorCollector.getFatalErrors().isEmpty());

assertEquals(1, statementCollector.getStatements().size());
Statement st = statementCollector.getStatements().iterator().next();
assertEquals(vf.createIRI("http://example.org/alice"), st.getSubject());
assertEquals(vf.createIRI("http://example.org/age"), st.getPredicate());
assertTrue(st.getObject() instanceof Literal);
Literal lit = (Literal) st.getObject();
assertEquals("a", lit.getLabel());
assertEquals(XSD.STRING, lit.getDatatype());
}

@Test
public void testParseIllegalURIFatal() throws IOException {
String data = " <urn:foo_bar\\r> <urn:foo> <urn:bar> ; <urn:foo2> <urn:bar2> . <urn:foobar> <urn:food> <urn:barf> . ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.ModelFactory;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.impl.DynamicModelFactory;
import org.eclipse.rdf4j.sail.SailException;
Expand Down Expand Up @@ -70,7 +71,7 @@ class SailSourceBranch implements SailSource {

/**
* The {@link Model} instances that should be used to store {@link SailSink#approve(Resource, IRI, Value, Resource)}
* and {@link SailSink#deprecate(Resource, IRI, Value, Resource)} statements.
* and {@link SailSink#deprecate(Statement)} statements.
*/
private final ModelFactory modelFactory;

Expand Down Expand Up @@ -306,7 +307,7 @@ public void flush() throws SailException {
prepared = null;
}
}
} catch (SailException e) {
} catch (Throwable e) {
// clear changes if flush fails
changes.clear();
prepared = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.EvaluationStatistics;
import org.eclipse.rdf4j.sail.Sail;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.base.BackingSailSource;
import org.eclipse.rdf4j.sail.base.Changeset;
Expand Down Expand Up @@ -389,7 +390,11 @@ public synchronized void flush() throws SailException {
throw new SailException(e);
} catch (RuntimeException e) {
logger.error("Encountered an unexpected problem while trying to commit", e);
throw e;
if (e instanceof SailException) {
throw e;
}
// Ensure upstream handles this as a SailException so branch flush clears pending changes
throw new SailException(e);
} finally {
sinkStoreAccessLock.unlock();
}
Expand Down Expand Up @@ -445,7 +450,15 @@ public void clear(Resource... contexts) throws SailException {

@Override
public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException {
addStatement(subj, pred, obj, explicit, ctx);
try {
addStatement(subj, pred, obj, explicit, ctx);
} catch (RuntimeException e) {
if (e instanceof SailException) {
throw e;
}
// Ensure upstream handles this as a SailException so branch flush clears pending changes
throw new SailException(e);
}
}

@Override
Expand Down Expand Up @@ -478,8 +491,11 @@ public void approveAll(Set<Statement> approved, Set<Resource> approvedContexts)
} catch (IOException e) {
throw new SailException(e);
} catch (RuntimeException e) {
if (e instanceof SailException) {
throw e;
}
logger.error("Encountered an unexpected problem while trying to add a statement", e);
throw e;
throw new SailException(e);
} finally {
sinkStoreAccessLock.unlock();
}
Expand Down Expand Up @@ -540,8 +556,11 @@ private boolean addStatement(Resource subj, IRI pred, Value obj, boolean explici
} catch (IOException e) {
throw new SailException(e);
} catch (RuntimeException e) {
if (e instanceof SailException) {
throw e;
}
logger.error("Encountered an unexpected problem while trying to add a statement", e);
throw e;
throw new SailException(e);
} finally {
sinkStoreAccessLock.unlock();
}
Expand Down Expand Up @@ -614,7 +633,11 @@ private long removeStatements(Resource subj, IRI pred, Value obj, boolean explic
throw new SailException(e);
} catch (RuntimeException e) {
logger.error("Encountered an unexpected problem while trying to remove statements", e);
throw e;
if (e instanceof SailException) {
throw e;
}
// Ensure upstream handles this as a SailException so branch flush clears pending changes
throw new SailException(e);
} finally {
sinkStoreAccessLock.unlock();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ protected void commitInternal() throws SailException {
try {
super.commitInternal();
} finally {
txnLock.release();
txnLock = null;
if (txnLock != null) {
txnLock.release();
txnLock = null;
}
}

nativeStore.notifySailChanged(sailChangedEvent);
Expand All @@ -91,8 +93,10 @@ protected void rollbackInternal() throws SailException {
try {
super.rollbackInternal();
} finally {
txnLock.release();
txnLock = null;
if (txnLock != null) {
txnLock.release();
txnLock = null;
}
}
// create a fresh event object.
sailChangedEvent = new DefaultSailChangedEvent(nativeStore);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2025 Eclipse RDF4J contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.rdf4j.sail.nativerdf;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.Map;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.base.SailSink;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Verifies that NativeSailStore wraps unexpected RuntimeExceptions in SailException so upstream callers can reliably
* handle failures (e.g., branch flush clearing pending changes).
*/
public class NativeSailStoreRuntimeWrappingIT {

private File dataDir;
private NativeSailStore store;

@BeforeEach
public void setUp() throws Exception {
dataDir = Files.createTempDirectory("nativestore-wrap-test").toFile();
store = new NativeSailStore(dataDir, "spoc");
}

@AfterEach
public void tearDown() throws Exception {
if (store != null) {
store.close();
}
}

@Test
public void testFlushWrapsRuntimeIntoSailException() throws Exception {
// Replace TripleStore with a stub that throws at commit()
replaceTripleStore(new ThrowOnCommitTripleStore(dataDir, "spoc"));

// Add a statement to ensure a triplestore transaction is started
SailSink sink = store.getExplicitSailSource().sink(null);
ValueFactory vf = store.getValueFactory();
IRI s = vf.createIRI("urn:s");
IRI p = vf.createIRI("urn:p");
IRI o = vf.createIRI("urn:o");
sink.approve(s, p, o, (Resource) null);

// Now flush, expecting a SailException (wrapping the runtime)
assertThatThrownBy(() -> sink.flush())
.isInstanceOf(SailException.class);
}

@Test
public void testRemoveStatementsWrapsRuntimeIntoSailException() throws Exception {
// Replace TripleStore with a stub that throws at removeTriplesByContext()
replaceTripleStore(new ThrowOnRemoveTripleStore(dataDir, "spoc"));

SailSink sink = store.getExplicitSailSource().sink(null);
// Expect SailException when attempting to remove (deprecateByQuery delegates)
assertThatThrownBy(() -> sink.deprecateByQuery(null, null, null, new Resource[] { null }))
.isInstanceOf(SailException.class);
}

private void replaceTripleStore(TripleStore newTripleStore) throws Exception {
Field f = NativeSailStore.class.getDeclaredField("tripleStore");
f.setAccessible(true);
f.set(store, newTripleStore);
}

private static class ThrowOnCommitTripleStore extends TripleStore {
public ThrowOnCommitTripleStore(File dir, String indexSpecStr) throws Exception {
super(dir, indexSpecStr, false);
}

@Override
public void commit() {
throw new RuntimeException("simulated failure during commit");
}
}

private static class ThrowOnRemoveTripleStore extends TripleStore {
public ThrowOnRemoveTripleStore(File dir, String indexSpecStr) throws Exception {
super(dir, indexSpecStr, false);
}

@Override
public Map<Integer, Long> removeTriplesByContext(int subjID, int predID, int objID, int contextId,
boolean explicit) {
throw new RuntimeException("simulated failure during removeTriplesByContext");
}

@Override
public void startTransaction() {
// no-op; we're only interested in remove path throwing
}
}
}
Loading
Loading