SnapshotSailStoreTest.java

/*******************************************************************************
 * Copyright (c) 2022 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.base;

import java.util.function.Function;

import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.common.iteration.EmptyIteration;
import org.eclipse.rdf4j.common.transaction.IsolationLevel;
import org.eclipse.rdf4j.common.transaction.IsolationLevels;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.LinkedHashModel;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.algebra.evaluation.federation.FederatedServiceResolver;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.EvaluationStatistics;
import org.eclipse.rdf4j.sail.NotifyingSailConnection;
import org.eclipse.rdf4j.sail.Sail;
import org.eclipse.rdf4j.sail.SailConnection;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.helpers.AbstractNotifyingSail;
import org.junit.jupiter.api.Test;

/**
 * Minimal tests for the functionality of {@link SnapshotSailStore}
 */
public class SnapshotSailStoreTest {

	/**
	 * Base no-op sink as base for testing
	 */
	static class TestSailSink implements SailSink {

		@Override
		public void close() throws SailException {

		}

		@Override
		public void prepare() throws SailException {

		}

		@Override
		public void flush() throws SailException {

		}

		@Override
		public void setNamespace(String prefix, String name) throws SailException {

		}

		@Override
		public void removeNamespace(String prefix) throws SailException {

		}

		@Override
		public void clearNamespaces() throws SailException {

		}

		@Override
		public void clear(Resource... contexts) throws SailException {

		}

		@Override
		public void observe(Resource subj, IRI pred, Value obj, Resource... contexts) throws SailException {

		}

		@Override
		public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException {

		}

		@Override
		public void deprecate(Statement statement) throws SailException {

		}
	}

	@Test
	public void testRollbackExceptionDuringCommit() {
		SnapshotSailStore sailStore = createSnapshotSailStore(level -> new TestSailSink() {
			@Override
			public void approve(Resource subj, IRI pred, Value obj, Resource ctx) throws SailException {
				throw new SailException("error during approve");
			}
		});

		Sail sail = createSail(sailStore);
		SailConnection c = sail.getConnection();
		c.begin(IsolationLevels.SNAPSHOT);
		c.addStatement(RDF.TYPE, RDFS.LABEL, sail.getValueFactory().createLiteral("type"));
		try {
			// an exception is triggered during commit by sink implementation above
			c.commit();
		} catch (Exception e) {
			c.rollback();
		} finally {
			c.close();
			// shutting down the SAIL should not result in an exception
			sail.shutDown();
		}
	}

	private Sail createSail(SailStore sailStore) {
		return new AbstractNotifyingSail() {
			@Override
			protected void shutDownInternal() throws SailException {
				// closing the SailStore tries to flush existing changes again
				sailStore.close();
			}

			@Override
			protected NotifyingSailConnection getConnectionInternal() throws SailException {
				return new SailSourceConnection(this, sailStore, (FederatedServiceResolver) null) {
					@Override
					protected void addStatementInternal(Resource subj, IRI pred, Value obj, Resource... contexts)
							throws SailException {
					}

					@Override
					protected void removeStatementsInternal(Resource subj, IRI pred, Value obj, Resource... contexts)
							throws SailException {
					}
				};
			}

			@Override
			public boolean isWritable() throws SailException {
				return true;
			}

			@Override
			public ValueFactory getValueFactory() {
				return SimpleValueFactory.getInstance();
			}
		};
	}

	private SnapshotSailStore createSnapshotSailStore(Function<IsolationLevel, SailSink> sinkFactory) {
		BackingSailSource dummySource = new BackingSailSource() {
			@Override
			public SailSink sink(IsolationLevel level) throws SailException {
				return sinkFactory.apply(level);
			}

			@Override
			public SailDataset dataset(IsolationLevel level) throws SailException {
				return new SailDataset() {
					@Override
					public void close() throws SailException {
					}

					@Override
					public CloseableIteration<? extends Namespace> getNamespaces() throws SailException {
						return new EmptyIteration<>();
					}

					@Override
					public String getNamespace(String prefix) throws SailException {
						return null;
					}

					@Override
					public CloseableIteration<? extends Resource> getContextIDs() throws SailException {
						return new EmptyIteration<>();
					}

					@Override
					public CloseableIteration<? extends Statement> getStatements(Resource subj, IRI pred,
							Value obj,
							Resource... contexts) throws SailException {
						return new EmptyIteration<>();
					}
				};
			}
		};
		return new SnapshotSailStore(new SailStore() {
			@Override
			public ValueFactory getValueFactory() {
				return SimpleValueFactory.getInstance();
			}

			@Override
			public EvaluationStatistics getEvaluationStatistics() {
				return new EvaluationStatistics();
			}

			@Override
			public SailSource getExplicitSailSource() {
				return dummySource;
			}

			@Override
			public SailSource getInferredSailSource() {
				return dummySource;
			}

			@Override
			public void close() throws SailException {
			}
		}, LinkedHashModel::new);
	}
}