TransactionSettingsTest.java

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

import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.PerformanceHint.CacheDisabled;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.PerformanceHint.CacheEnabled;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.PerformanceHint.ParallelValidation;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.PerformanceHint.SerialValidation;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach.Auto;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach.Bulk;
import static org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach.Disabled;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.InputStream;

import org.eclipse.rdf4j.common.transaction.IsolationLevels;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class TransactionSettingsTest {

	@Test
	public void testBulk() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(Bulk);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Bulk);
			Assertions.assertFalse(transactionSettings.isCacheSelectNodes());
			Assertions.assertFalse(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}

	}

	@Test
	public void testBulkParallel() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(Bulk, ParallelValidation);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Bulk);
			Assertions.assertFalse(transactionSettings.isCacheSelectNodes());
			Assertions.assertTrue(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testBulkParallelCache() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(Bulk, ParallelValidation, CacheEnabled);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Bulk);
			Assertions.assertTrue(transactionSettings.isCacheSelectNodes());
			Assertions.assertTrue(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testParallelCacheEmptyRepo() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(ParallelValidation, CacheEnabled);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Bulk);
			Assertions.assertTrue(transactionSettings.isCacheSelectNodes());
			Assertions.assertTrue(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testDefault() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin();

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Auto);
			Assertions.assertTrue(transactionSettings.isCacheSelectNodes());
			Assertions.assertTrue(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testNulls() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin();

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertNotNull(transactionSettings.getValidationApproach());
			Assertions.assertTrue(transactionSettings.isCacheSelectNodes());
			Assertions.assertTrue(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testDefaultOverride() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);
		shaclSail.setCacheSelectNodes(true);

		SailRepository sailRepository = new SailRepository(shaclSail);

		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(CacheDisabled, SerialValidation);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Auto);
			Assertions.assertFalse(transactionSettings.isCacheSelectNodes());
			Assertions.assertFalse(transactionSettings.isParallelValidation());
			Assertions.assertSame(transactionSettings.getIsolationLevel(), IsolationLevels.SNAPSHOT_READ);

			connection.commit();

		}

		sailRepository.shutDown();

	}

	private void addDummyData(SailRepository sailRepository) {
		try (SailRepositoryConnection connection1 = sailRepository.getConnection()) {
			connection1.add(RDF.TYPE, RDF.TYPE, RDF.PROPERTY);
		}
	}

	@Test
	public void testSerializableParallelValidation() {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setParallelValidation(true);

		SailRepository sailRepository = new SailRepository(shaclSail);
		addDummyData(sailRepository);

		try (SailRepositoryConnection connection = sailRepository.getConnection()) {

			connection.begin(IsolationLevels.SERIALIZABLE, ParallelValidation);

			ShaclSailConnection sailConnection = (ShaclSailConnection) connection.getSailConnection();
			ShaclSailConnection.Settings transactionSettings = sailConnection.getTransactionSettings();

			Assertions.assertSame(transactionSettings.getValidationApproach(), Auto);
			Assertions.assertFalse(transactionSettings.isParallelValidation());

			connection.commit();

		} finally {
			sailRepository.shutDown();
		}
	}

	@Test
	public void testPriority() {
		// test default behaviour
		Assertions.assertEquals(Auto, ShaclSailConnection.Settings.getMostSignificantValidationApproach(null, null));

		// test single null
		Assertions.assertEquals(Bulk, ShaclSailConnection.Settings.getMostSignificantValidationApproach(Bulk, null));
		Assertions.assertEquals(Bulk, ShaclSailConnection.Settings.getMostSignificantValidationApproach(null, Bulk));

		// test base overrides transaction
		Assertions.assertEquals(Bulk, ShaclSailConnection.Settings.getMostSignificantValidationApproach(Bulk, Auto));
		Assertions.assertEquals(Disabled,
				ShaclSailConnection.Settings.getMostSignificantValidationApproach(Disabled, Auto));
		Assertions.assertEquals(Disabled,
				ShaclSailConnection.Settings.getMostSignificantValidationApproach(Disabled, Bulk));

		// test transaction overrides base
		Assertions.assertEquals(Bulk, ShaclSailConnection.Settings.getMostSignificantValidationApproach(Auto, Bulk));
		Assertions.assertEquals(Disabled,
				ShaclSailConnection.Settings.getMostSignificantValidationApproach(Auto, Disabled));
		Assertions.assertEquals(Disabled,
				ShaclSailConnection.Settings.getMostSignificantValidationApproach(Bulk, Disabled));

	}

	@Test
	public void testValid() throws Exception {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Bulk, IsolationLevels.NONE);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);
			connection.add(RDFS.RESOURCE, RDFS.LABEL, connection.getValueFactory().createLiteral("a"));
			connection.commit();

		} finally {
			repository.shutDown();
		}

	}

	@Test
	public void testInvalid() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Bulk, IsolationLevels.NONE);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			assertThrows(ShaclSailValidationException.class, () -> {
				try {
					connection.commit();
				} catch (RepositoryException e) {
					throw e.getCause();
				}
			});

		} finally {
			repository.shutDown();
		}
	}

	@Test
	public void testInvalidSnapshot() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Bulk, IsolationLevels.SNAPSHOT);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);
			assertThrows(ShaclSailValidationException.class, () -> {
				try {
					connection.commit();
				} catch (RepositoryException e) {
					throw e.getCause();
				}
			});

		} finally {
			repository.shutDown();
		}

	}

	@Test
	public void testInvalidRollsBackCorrectly() {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Bulk, IsolationLevels.NONE);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			connection.commit();

		} catch (Exception ignored) {

		}

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(IsolationLevels.NONE);
			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);
			connection.commit();

		} finally {
			repository.shutDown();
		}

	}

	@Test
	public void testValidationDisabled() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Disabled);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			connection.commit();

			connection.begin(Bulk);
			try (SailRepositoryConnection connection1 = repository.getConnection()) {

				assertThrows(ShaclSailValidationException.class, () -> {
					try {
						connection.commit();
					} catch (RepositoryException e) {
						throw e.getCause();
					}
				});
			}

		} finally {
			repository.shutDown();
		}

	}

	@Test
	public void testValidationDisabledSnapshotSerializableValidation() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Disabled, IsolationLevels.SNAPSHOT);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.commit();

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.CLASS);

			connection.begin(Disabled, IsolationLevels.SNAPSHOT);

			try (SailRepositoryConnection connection1 = repository.getConnection()) {

				connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

				connection.commit();

			}

		} finally {
			repository.shutDown();
		}

	}

	@Test
	public void testDisabledValidationBulk() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		((ShaclSail) repository.getSail()).disableValidation();

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Bulk);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			connection.commit();

		} finally {
			repository.shutDown();
		}
	}

	@Test
	public void testDisabledValidationAuto() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		addDummyData(repository);

		((ShaclSail) repository.getSail()).disableValidation();

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Auto);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.commit();

			connection.begin(Auto);

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			connection.commit();

		} finally {
			repository.shutDown();
		}
	}

	@Test
	public void testDisabledValidationAutoEmptyRepo() throws Throwable {

		SailRepository repository = new SailRepository(new ShaclSail(new MemoryStore()));
		((ShaclSail) repository.getSail()).disableValidation();

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin(Auto);

			try (InputStream shapesData = Utils.class.getClassLoader().getResourceAsStream("shacl.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.commit();

			connection.begin(Auto);

			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);

			connection.commit();

		} finally {
			repository.shutDown();
		}
	}

	@Test
	public void testSerialOverrideWithModifiedShapes() throws Throwable {

		ShaclSail sail = new ShaclSail(new MemoryStore());
		ShaclSail spy = Mockito.spy(sail);
		SailRepository repository = new SailRepository(spy);
		addDummyData(repository);

		try (RepositoryConnection connection = repository.getConnection()) {

			connection.begin();

			connection.add(RDFS.RESOURCE, RDF.TYPE, FOAF.PERSON);
			connection.add(RDFS.RESOURCE, FOAF.AGE, connection.getValueFactory().createLiteral(1));
			connection.add(RDFS.RESOURCE, FOAF.AGE, connection.getValueFactory().createLiteral(2));

			connection.commit();

			connection.begin(SerialValidation);

			try (InputStream shapesData = Utils.class.getClassLoader()
					.getResourceAsStream("shaclDatatypeAndMinCount.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.commit();

			Mockito.verify(spy, Mockito.never()).submitToExecutorService(Mockito.any());

			connection.clear(RDF4J.SHACL_SHAPE_GRAPH);

			Mockito.verify(spy, Mockito.never()).submitToExecutorService(Mockito.any());

			connection.begin(ParallelValidation);

			try (InputStream shapesData = Utils.class.getClassLoader()
					.getResourceAsStream("shaclDatatypeAndMinCount.trig")) {
				connection.add(shapesData, "", RDFFormat.TRIG, RDF4J.SHACL_SHAPE_GRAPH);
			}

			connection.commit();

			Mockito.verify(spy, Mockito.atLeastOnce()).submitToExecutorService(Mockito.any());

		} finally {
			repository.shutDown();
		}
	}

}