NativeSailStoreRuntimeWrappingIT.java

/*
 * 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
		}
	}
}