jena-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From a...@apache.org
Subject [03/13] jena git commit: JENA-624: New transactional in-memory Dataset with MR+SW locking
Date Sat, 14 Nov 2015 14:42:11 GMT
http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TripleTableForm.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TripleTableForm.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TripleTableForm.java
new file mode 100644
index 0000000..81cb7c6
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TripleTableForm.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static java.util.Arrays.stream;
+import static java.util.EnumSet.of;
+import static org.apache.jena.sparql.core.mem.TupleSlot.*;
+
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+/**
+ * Forms for triple indexes.
+ *
+ */
+public enum TripleTableForm implements Supplier<TripleTable>,Predicate<Set<TupleSlot>> {
+
+	/**
+	 * Subject-predicate-object.
+	 */
+	SPO(of(SUBJECT, PREDICATE), SUBJECT) {
+		@Override
+		public TripleTable get() {
+			return new PMapTripleTable(name()) {
+
+				@Override
+				public Stream<Triple> find(final Node s, final Node p, final Node o) {
+					return _find(s, p, o);
+				}
+
+				@Override
+				public void add(final Triple t) {
+					_add(t.getSubject(), t.getPredicate(), t.getObject());
+				}
+
+				@Override
+				public void delete(final Triple t) {
+					_delete(t.getSubject(), t.getPredicate(), t.getObject());
+				}
+
+			};
+		}
+
+	},
+	/**
+	 * Predicate-object-subject.
+	 */
+	POS(of(PREDICATE, OBJECT), PREDICATE) {
+
+		@Override
+		public TripleTable get() {
+			return new PMapTripleTable(name()) {
+
+				@Override
+				public Stream<Triple> find(final Node s, final Node p, final Node o) {
+					return _find(p, o, s);
+				}
+
+				@Override
+				public void add(final Triple t) {
+					_add(t.getPredicate(), t.getObject(), t.getSubject());
+				}
+
+				@Override
+				public void delete(final Triple t) {
+					_delete(t.getPredicate(), t.getObject(), t.getSubject());
+				}
+
+			};
+		}
+	},
+	/**
+	 * Object-subject-predicate.
+	 */
+	OSP(of(OBJECT, SUBJECT), OBJECT) {
+
+		@Override
+		public TripleTable get() {
+			return new PMapTripleTable(name()) {
+
+				@Override
+				public Stream<Triple> find(final Node s, final Node p, final Node o) {
+					return _find(o, s, p);
+				}
+
+				@Override
+				public void add(final Triple t) {
+					_add(t.getObject(), t.getSubject(), t.getPredicate());
+				}
+
+				@Override
+				public void delete(final Triple t) {
+					_delete(t.getObject(), t.getSubject(), t.getPredicate());
+				}
+
+			};
+		}
+	};
+	private TripleTableForm(final Set<TupleSlot> tp, final TupleSlot op) {
+		this.twoPrefix = tp;
+		this.onePrefix = of(op);
+	}
+
+	/**
+	 * Prefixes of the pattern for this table form.
+	 */
+	public final Set<TupleSlot> twoPrefix, onePrefix;
+
+	/**
+	 * @param pattern
+	 * @return whether this index form avoids traversal for a query of this pattern
+	 */
+	@Override
+	public boolean test(final Set<TupleSlot> pattern) {
+		return twoPrefix.equals(pattern) || onePrefix.equals(pattern) || pattern.size() == 3;
+	}
+
+	/**
+	 * @param pattern
+	 * @return the most appropriate choice of index form for that query
+	 */
+	public static TripleTableForm chooseFrom(final Set<TupleSlot> pattern) {
+		return tableForms().filter(f -> f.test(pattern)).findFirst().orElse(SPO);
+	}
+
+	/**
+	 * @return a stream of these table forms
+	 */
+	public static Stream<TripleTableForm> tableForms() {
+		return stream(values());
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleSlot.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleSlot.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleSlot.java
new file mode 100644
index 0000000..d46fdb4
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleSlot.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+/**
+ * The various possible positions of a node in a tuple.
+ *
+ */
+public enum TupleSlot {
+	GRAPH, SUBJECT, PREDICATE, OBJECT;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleTable.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleTable.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleTable.java
new file mode 100644
index 0000000..444292e
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/TupleTable.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import org.apache.jena.sparql.core.Transactional;
+
+/**
+ * A mutable table of tuples. The expectation is that some kind of query functionality will be provided by subtypes.
+ *
+ * @param <TupleType> the type of tuple stored herein
+ */
+public interface TupleTable<TupleType> extends Transactional {
+
+	/**
+	 * Add a tuple to the table
+	 *
+	 * @param t the tuple to add
+	 */
+	void add(TupleType t);
+
+	/**
+	 * Remove a tuple from the table
+	 *
+	 * @param t the tuple to remove
+	 */
+	void delete(TupleType t);
+
+	@Override
+	default void abort() {
+		end();
+	}
+
+	/**
+	 * Clear all tuples from this table.
+	 */
+	void clear();
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/package-info.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/package-info.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/package-info.java
new file mode 100644
index 0000000..7f15577
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Types to support efficient in-memory dataset operations
+ */
+package org.apache.jena.sparql.core.mem;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/TS_Core.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/TS_Core.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/TS_Core.java
index 0fd1190..9e0499d 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/core/TS_Core.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/TS_Core.java
@@ -18,7 +18,9 @@
 
 package org.apache.jena.sparql.core;
 
+import org.apache.jena.sparql.core.assembler.TestAssembler;
 import org.apache.jena.sparql.core.journaling.TestJournaling;
+import org.apache.jena.sparql.core.mem.TestInMemory;
 import org.junit.runner.RunWith ;
 import org.junit.runners.Suite ;
 
@@ -35,6 +37,8 @@ import org.junit.runners.Suite ;
     , TestDatasetMonitor.class
     , TestDatasetGraphWithLock.class
     , TestJournaling.class
+    , TestInMemory.class
+    , TestAssembler.class
 })
 
 public class TS_Core

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestAssembler.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestAssembler.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestAssembler.java
new file mode 100644
index 0000000..2adb5e7
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestAssembler.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.assembler;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({ TestInMemDatasetAssembler.class })
+public class TestAssembler {}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestInMemDatasetAssembler.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestInMemDatasetAssembler.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestInMemDatasetAssembler.java
new file mode 100644
index 0000000..d90edd2
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/assembler/TestInMemDatasetAssembler.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.assembler;
+
+import static java.nio.file.Files.createTempFile;
+import static org.apache.jena.assembler.JA.*;
+import static org.apache.jena.assembler.Mode.DEFAULT;
+import static org.apache.jena.query.DatasetFactory.createTxnMem;
+import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel;
+import static org.apache.jena.riot.Lang.NQUADS;
+import static org.apache.jena.riot.RDFDataMgr.write;
+import static org.apache.jena.riot.RDFFormat.NTRIPLES;
+import static org.apache.jena.sparql.core.assembler.DatasetAssemblerVocab.pGraphName;
+import static org.apache.jena.sparql.core.assembler.DatasetAssemblerVocab.pNamedGraph;
+import static org.apache.jena.vocabulary.RDF.type;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+import org.apache.jena.assembler.exceptions.CannotConstructException;
+import org.apache.jena.graph.Node;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.Statement;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests for {@link InMemDatasetAssembler}
+ *
+ */
+public class TestInMemDatasetAssembler extends Assert {
+
+	/**
+	 * @param example
+	 */
+	private Dataset assemble(final Resource example) {
+		final InMemDatasetAssembler testAssembler = new InMemDatasetAssembler();
+		return testAssembler.open(testAssembler, example, DEFAULT);
+	}
+
+	@Test
+	public void directDataLinkForDefaultAndNamedGraphs() throws IOException {
+		// first make a file of triples to load later
+		final Model model = createDefaultModel();
+		final Path triples = createTempFile("simpleExample", ".nt");
+		final Resource triplesURI = model.createResource(triples.toFile().toURI().toString());
+		final Resource simpleExample = model.createResource("test:simpleExample");
+		simpleExample.addProperty(type, MemoryDataset);
+		// add a default graph
+		simpleExample.addProperty(data, triplesURI);
+		// add a named graph
+		final Resource namedGraphDef = model.createResource("test:namedGraphDef");
+		simpleExample.addProperty(pNamedGraph, namedGraphDef);
+		final Resource namedGraphName = model.createResource("test:namedGraphExample");
+		namedGraphDef.addProperty(type, MemoryModel);
+		namedGraphDef.addProperty(pGraphName, namedGraphName);
+		namedGraphDef.addProperty(data, triplesURI);
+
+		try (OutputStream out = new FileOutputStream(triples.toFile())) {
+			write(out, model, NTRIPLES);
+		}
+
+		final Dataset dataset = assemble(simpleExample);
+		final Model assembledDefaultModel = dataset.getDefaultModel();
+		final Model assembledNamedModel = dataset.getNamedModel(namedGraphName.getURI());
+
+		// we put the same triples in each model, so we check for the same triples in each model
+		for (final Model m : new Model[] { assembledDefaultModel, assembledNamedModel }) {
+			assertTrue(m.contains(simpleExample, pNamedGraph, namedGraphDef));
+			assertTrue(m.contains(namedGraphDef, pGraphName, namedGraphName));
+			assertTrue(m.contains(simpleExample, data, triplesURI));
+
+		}
+		final Iterator<Node> graphNodes = dataset.asDatasetGraph().listGraphNodes();
+		assertTrue(graphNodes.hasNext());
+		assertEquals(namedGraphName.asNode(), graphNodes.next());
+		assertFalse(graphNodes.hasNext());
+	}
+
+	@Test
+	public void directDataLinkToQuads() throws IOException {
+		// first make a file of quads to load later
+		final Model model = createDefaultModel();
+		final Path quads = createTempFile("quadExample", ".nq");
+		final Resource quadsURI = model.createResource(quads.toFile().toURI().toString());
+		final Resource simpleExample = model.createResource("test:simpleExample");
+		simpleExample.addProperty(type, MemoryDataset);
+		simpleExample.addProperty(data, quadsURI);
+
+		final DatasetGraph dsg = createTxnMem().asDatasetGraph();
+		model.listStatements().mapWith(Statement::asTriple).mapWith(t -> new Quad(quadsURI.asNode(), t))
+				.forEachRemaining(dsg::add);
+		try (OutputStream out = new FileOutputStream(quads.toFile())) {
+			write(out, dsg, NQUADS);
+		}
+
+		final Dataset dataset = assemble(simpleExample);
+		final Model assembledDefaultModel = dataset.getDefaultModel();
+		final Model assembledNamedModel = dataset.getNamedModel(quadsURI.getURI());
+		assertTrue(assembledDefaultModel.isEmpty());
+		assertTrue(assembledNamedModel.contains(assembledNamedModel.createStatement(simpleExample, data, quadsURI)));
+	}
+
+	@Test(expected = CannotConstructException.class)
+	public void wrongKindOfAssemblerDefinition() {
+		final Model model = createDefaultModel();
+		final Resource badExample = model.createResource("test:badExample");
+		assemble(badExample);
+	}
+
+	public void emptyDataset() {
+		final Model model = createDefaultModel();
+		final Resource empty = model.createResource("test:empty");
+		empty.addProperty(type, MemoryDataset);
+		assertTrue(assemble(empty).asDatasetGraph().isEmpty());
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/QuadTableTest.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/QuadTableTest.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/QuadTableTest.java
new file mode 100644
index 0000000..ca4ecc5
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/QuadTableTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static java.util.EnumSet.allOf;
+import static org.apache.jena.ext.com.google.common.collect.Sets.powerSet;
+import static org.apache.jena.graph.NodeFactory.createURI;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.sparql.core.Quad;
+
+public abstract class QuadTableTest extends TupleTableTest<Quad, QuadTable> {
+
+	protected static final Node sampleNode = createURI("info:test");
+	private static final Quad q = Quad.create(sampleNode, sampleNode, sampleNode, sampleNode);
+
+	@Override
+	protected Quad testTuple() {
+		return q;
+	}
+
+	@Override
+	public Stream<Set<TupleSlot>> queryPatterns() {
+		return quadQueryPatterns();
+	}
+
+	static Stream<Set<TupleSlot>> quadQueryPatterns() {
+		return powerSet(allOf(TupleSlot.class)).stream();
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestDatasetGraphInMemory.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestDatasetGraphInMemory.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestDatasetGraphInMemory.java
new file mode 100644
index 0000000..f1466b5
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestDatasetGraphInMemory.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static org.apache.jena.atlas.iterator.Iter.iter;
+import static org.apache.jena.graph.Node.ANY;
+import static org.apache.jena.graph.NodeFactory.createBlankNode;
+import static org.apache.jena.graph.NodeFactory.createURI;
+import static org.apache.jena.query.ReadWrite.READ;
+import static org.apache.jena.query.ReadWrite.WRITE;
+import static org.apache.jena.sparql.core.Quad.unionGraph;
+import static org.apache.jena.sparql.graph.GraphFactory.createGraphMem;
+import static org.junit.Assert.*;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.query.DatasetFactory;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.sparql.core.AbstractDatasetGraphTests;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.core.Quad;
+import org.apache.jena.sparql.core.TestDatasetGraphViewGraphs;
+import org.apache.jena.sparql.core.TestDatasetGraphWithLock;
+import org.apache.jena.sparql.core.mem.TestDatasetGraphInMemory.TestDatasetGraphInMemoryBasic;
+import org.apache.jena.sparql.core.mem.TestDatasetGraphInMemory.TestDatasetGraphInMemoryLock;
+import org.apache.jena.sparql.core.mem.TestDatasetGraphInMemory.TestDatasetGraphInMemoryThreading;
+import org.apache.jena.sparql.core.mem.TestDatasetGraphInMemory.TestDatasetGraphInMemoryTransactions;
+import org.apache.jena.sparql.core.mem.TestDatasetGraphInMemory.TestDatasetGraphInMemoryViews;
+import org.apache.jena.sparql.transaction.AbstractTestTransaction;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+import org.slf4j.Logger;
+
+@RunWith(Suite.class)
+@SuiteClasses({ TestDatasetGraphInMemoryBasic.class, TestDatasetGraphInMemoryViews.class,
+		TestDatasetGraphInMemoryLock.class, TestDatasetGraphInMemoryThreading.class,
+		TestDatasetGraphInMemoryTransactions.class })
+public class TestDatasetGraphInMemory {
+
+	public static class TestDatasetGraphInMemoryThreading extends Assert {
+
+		Logger log = getLogger(TestDatasetGraphInMemoryThreading.class);
+
+		Quad q = Quad.create(createBlankNode(), createBlankNode(), createBlankNode(), createBlankNode());
+
+		@Test
+		public void abortedChangesNeverBecomeVisible() {
+			final DatasetGraphInMemory dsg = new DatasetGraphInMemory();
+			// flags with which to interleave threads
+			final AtomicBoolean addedButNotAborted = new AtomicBoolean(false);
+			final AtomicBoolean addedCheckedButNotAborted = new AtomicBoolean(false);
+			final AtomicBoolean aborted = new AtomicBoolean(false);
+
+			dsg.begin(READ);
+			assertTrue(dsg.isEmpty()); // no quads present
+			dsg.end();
+
+			// we introduce a Writer thread
+			new Thread() {
+
+				@Override
+				public void run() {
+					dsg.begin(WRITE);
+					log.debug("Writer: Added test quad.");
+					dsg.add(q);
+					assertFalse(dsg.isEmpty()); // quad has appeared in this transaction
+					addedButNotAborted.set(true);
+					log.debug("Writer: Waiting to abort addition of test quad.");
+					await().untilTrue(addedCheckedButNotAborted);
+					assertFalse(dsg.isEmpty()); // quad has appeared, but only inside this transaction
+					log.debug("Writer: Aborting test quad.");
+					dsg.abort();
+					log.debug("Writer: Aborted test quad.");
+					aborted.set(true);
+				}
+			}.start();
+			// back to Reader code
+			log.debug("Reader: Waiting for test quad to be added in Writer thread.");
+			await().untilTrue(addedButNotAborted);
+			dsg.begin(READ);
+			assertTrue(dsg.isEmpty()); // no quads present to Reader
+			dsg.end();
+			log.debug("Reader: Checked to see test quad is not visible.");
+			addedCheckedButNotAborted.set(true);
+			log.debug("Reader: Waiting to see Writer transaction aborted.");
+			await().untilTrue(aborted);
+			dsg.begin(READ);
+			assertTrue(dsg.isEmpty()); // no quads have appeared
+			dsg.end();
+		}
+
+		@Test
+		public void snapshotsShouldBeIsolated() {
+			final DatasetGraphInMemory dsg = new DatasetGraphInMemory();
+			// flags with which to interleave threads
+			final AtomicBoolean addedButNotCommitted = new AtomicBoolean(false);
+			final AtomicBoolean addedCheckedButNotCommitted = new AtomicBoolean(false);
+			final AtomicBoolean committed = new AtomicBoolean(false);
+
+			dsg.begin(READ);
+			assertTrue(dsg.isEmpty()); // no quads present
+			dsg.end();
+
+			// we introduce a Writer thread
+			new Thread() {
+
+				@Override
+				public void run() {
+					dsg.begin(WRITE);
+					log.debug("Writer: Added test quad.");
+					dsg.add(q);
+					assertFalse(dsg.isEmpty()); // quad has appeared, but only in this transaction
+					addedButNotCommitted.set(true);
+					log.debug("Writer: Waiting to commit test quad.");
+					await().untilTrue(addedCheckedButNotCommitted);
+					log.debug("Writer: Committing test quad.");
+					dsg.commit();
+					log.debug("Writer: Committed test quad.");
+					committed.set(true);
+				}
+			}.start();
+			// back to Reader code
+			log.debug("Reader: Waiting for test quad to be added in Writer thread.");
+			await().untilTrue(addedButNotCommitted);
+
+			dsg.begin(READ);
+			assertTrue(dsg.isEmpty()); // still no quads present, because Reader and Writer are isolated
+			log.debug("Reader: Checked to see test quad is not yet visible.");
+			addedCheckedButNotCommitted.set(true);
+			log.debug("Reader: Waiting to see test quad committed.");
+			await().untilTrue(committed);
+			assertTrue(dsg.isEmpty()); // still no quads present, because Reader and Writer are isolated
+			dsg.end();
+			// but a new transaction should see the results of Writer's action
+			dsg.begin(READ);
+			assertFalse(dsg.isEmpty()); // quad has appeared, for new transaction
+			dsg.end();
+		}
+
+		@Test
+		public void locksAreCorrectlyDistributed() {
+			final DatasetGraphInMemory dsg = new DatasetGraphInMemory();
+			final AtomicBoolean readLockCaptured = new AtomicBoolean(false);
+			final AtomicBoolean writeLockCaptured = new AtomicBoolean(false);
+
+			dsg.begin(WRITE); // acquire the write lock: no other Thread can now acquire it until it is released
+
+			new Thread() {
+
+				@Override
+				public void run() {
+					dsg.begin(READ); // a read lock should always be available except during a commit
+					readLockCaptured.set(true);
+					dsg.end();
+
+					dsg.begin(WRITE); // this should block until the write lock is released
+					writeLockCaptured.set(true);
+				}
+			}.start();
+			await().untilTrue(readLockCaptured);
+			if (writeLockCaptured.get()) fail("Write lock captured by two threads at once!");
+
+			dsg.end(); // release the write lock to competitor
+			await().untilTrue(writeLockCaptured);
+			assertTrue("Lock was not handed over to waiting thread!", writeLockCaptured.get());
+		}
+	}
+
+	public static class TestDatasetGraphInMemoryBasic extends AbstractDatasetGraphTests {
+
+		@Test
+		public void prefixesAreManaged() {
+			final Node graphName = createURI("http://example/g");
+			final DatasetGraph dsg = emptyDataset();
+			dsg.addGraph(graphName, createGraphMem());
+			final Dataset dataset = DatasetFactory.create(dsg);
+			Model model = dataset.getNamedModel(graphName.getURI());
+			final String testPrefix = "example";
+			final String testURI = "http://example/";
+			model.setNsPrefix(testPrefix, testURI);
+			assertEquals(testURI, model.getNsPrefixURI(testPrefix));
+			model.close();
+			model = dataset.getNamedModel(graphName.getURI());
+			final String nsURI = dataset.getNamedModel(graphName.getURI()).getNsPrefixURI(testPrefix);
+			assertNotNull(nsURI);
+			assertEquals(testURI, nsURI);
+		}
+
+		@Test
+		public void unionGraphWorksProperly() {
+			final DatasetGraph dsg = emptyDataset();
+			// quads from named graphs should appear in union
+			final Quad q = Quad.create(createBlankNode(), createBlankNode(), createBlankNode(), createBlankNode());
+			dsg.add(q);
+			assertTrue(iter(dsg.find(unionGraph, ANY, ANY, ANY)).some(q::equals));
+			// no triples from default graph should appear in union
+			final Triple t = Triple.create(createBlankNode(), createBlankNode(), createBlankNode());
+			dsg.getDefaultGraph().add(t);
+			assertFalse(iter(dsg.find(unionGraph, ANY, ANY, ANY)).some(t::equals));
+		}
+
+		@Override
+		protected DatasetGraph emptyDataset() {
+			return DatasetGraphFactory.createTxnMem();
+		}
+	}
+
+	public static class TestDatasetGraphInMemoryLock extends TestDatasetGraphWithLock {
+		@Override
+		protected Dataset createFixed() {
+			return DatasetFactory.createTxnMem();
+		}
+	}
+
+	public static class TestDatasetGraphInMemoryViews extends TestDatasetGraphViewGraphs {
+
+		@Override
+		protected DatasetGraph createBaseDSG() {
+			return DatasetGraphFactory.createTxnMem();
+		}
+	}
+
+	public static class TestDatasetGraphInMemoryTransactions extends AbstractTestTransaction {
+
+		@Override
+		protected Dataset create() {
+			return DatasetFactory.createTxnMem();
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestHexTable.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestHexTable.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestHexTable.java
new file mode 100644
index 0000000..43b5f5c
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestHexTable.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static java.util.stream.Collectors.toSet;
+import static org.apache.jena.ext.com.google.common.collect.ImmutableSet.of;
+import static org.apache.jena.graph.Node.ANY;
+import static org.apache.jena.graph.NodeFactory.createBlankNode;
+import static org.apache.jena.sparql.core.mem.TupleSlot.*;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.sparql.core.Quad;
+import org.junit.Test;
+
+public class TestHexTable extends QuadTableTest {
+
+	@Test
+	public void testListGraphNodes() {
+		final int nodesToTry = 50;
+		final HexTable index = new HexTable();
+		final Set<Node> graphNodes = new HashSet<>(nodesToTry);
+		index.begin(null);
+		for (int i = 0; i < nodesToTry; i++) {
+			final Node node = createBlankNode();
+			index.add(Quad.create(node, node, node, node));
+			graphNodes.add(node);
+			assertEquals(graphNodes, index.listGraphNodes().collect(toSet()));
+		}
+		index.end();
+	}
+
+	@Test
+	public void checkConcreteQueries() {
+		queryPatterns().filter(p -> !allWildcardQuery.equals(p)).map(TestHexTable::exampleFrom).forEach(testQuery -> {
+			final HexTable index = new HexTable();
+			index.begin(null);
+			// add our sample quad
+			index.add(testTuple());
+			// add a noise quad from which our sample should be distinguished
+			final Node node = createBlankNode();
+			final Quad noiseQuad = Quad.create(node, node, node, node);
+			index.add(noiseQuad);
+			index.commit();
+
+			index.begin(null);
+			Set<Quad> contents = index
+					.find(testQuery.getGraph(), testQuery.getSubject(), testQuery.getPredicate(), testQuery.getObject())
+					.collect(toSet());
+			assertEquals(of(testTuple()), contents);
+			// both Node.ANY and null should work as wildcards
+			contents = index.find(null, ANY, null, ANY).collect(toSet());
+			assertEquals(of(testTuple(), noiseQuad), contents);
+			index.end();
+		});
+	}
+
+	private static Quad exampleFrom(final Set<TupleSlot> pattern) {
+		return Quad.create(pattern.contains(GRAPH) ? sampleNode : ANY, pattern.contains(SUBJECT) ? sampleNode : ANY,
+				pattern.contains(PREDICATE) ? sampleNode : ANY, pattern.contains(OBJECT) ? sampleNode : ANY);
+	}
+
+	private final HexTable testTable = new HexTable();
+
+	@Override
+	protected QuadTable table() {
+		return testTable;
+	}
+
+	@Override
+	protected Stream<Quad> tuples() {
+		return table().find(ANY, ANY, ANY, ANY);
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestInMemory.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestInMemory.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestInMemory.java
new file mode 100644
index 0000000..1f7be0d
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestInMemory.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+/**
+ * Tests for in-memory Dataset and its default implementation.
+ *
+ */
+@RunWith(Suite.class)
+@SuiteClasses({ TestDatasetGraphInMemory.class, TestQuadTableForms.class, TestTripleTableForms.class,
+		TestHexTable.class, TestTriTable.class })
+public class TestInMemory {}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestQuadTableForms.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestQuadTableForms.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestQuadTableForms.java
new file mode 100644
index 0000000..8bd5898
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestQuadTableForms.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static org.apache.jena.ext.com.google.common.collect.ImmutableSet.of;
+import static org.apache.jena.graph.Node.ANY;
+import static org.apache.jena.sparql.core.mem.QuadTableForm.*;
+import static org.apache.jena.sparql.core.mem.TupleSlot.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.sparql.core.Quad;
+import org.junit.Test;
+
+public class TestQuadTableForms extends TupleTableFormsTest<QuadTableForm> {
+
+	@Override
+	protected Stream<QuadTableForm> tableForms() {
+		return QuadTableForm.tableForms();
+	}
+
+	@Override
+	protected Stream<Set<TupleSlot>> queryPatterns() {
+		return QuadTableTest.quadQueryPatterns();
+	}
+
+	private static Map<Set<TupleSlot>, Set<QuadTableForm>> answerKey = new HashMap<Set<TupleSlot>, Set<QuadTableForm>>() {
+		{
+			put(of(GRAPH), of(GSPO, GOPS));
+			put(of(GRAPH, SUBJECT), of(GSPO));
+			put(of(GRAPH, SUBJECT, PREDICATE), of(GSPO));
+			put(of(GRAPH, SUBJECT, OBJECT), of(OSGP));
+			put(of(SUBJECT), of(SPOG));
+			put(of(PREDICATE), of(PGSO));
+			put(of(GRAPH, PREDICATE), of(PGSO));
+			put(of(SUBJECT, PREDICATE), of(SPOG));
+			put(of(OBJECT), of(OPSG, OSGP));
+			put(of(GRAPH, OBJECT), of(GOPS));
+			put(of(SUBJECT, OBJECT), of(OSGP));
+			put(of(PREDICATE, OBJECT), of(OPSG));
+			put(of(GRAPH, PREDICATE, OBJECT), of(GOPS));
+			put(of(SUBJECT, PREDICATE, OBJECT), of(SPOG));
+			put(of(SUBJECT, PREDICATE, OBJECT, GRAPH), of(GSPO, GOPS, SPOG, OPSG, OSGP, PGSO));
+			put(of(), of(GSPO));
+		}
+	};
+
+	@Test
+	public void addAndRemoveSomeQuads() {
+		tableForms().map(QuadTableForm::get).map(table -> new QuadTableTest() {
+
+			@Override
+			protected QuadTable table() {
+				return table;
+			}
+
+			@Override
+			protected Stream<Quad> tuples() {
+				return table.find(ANY, ANY, ANY, ANY);
+			}
+		}).forEach(TupleTableTest::addAndRemoveSomeTuples);
+	}
+
+	@Override
+	protected QuadTableForm chooseFrom(final Set<TupleSlot> sample) {
+		return QuadTableForm.chooseFrom(sample);
+	}
+
+	@Override
+	protected Map<Set<TupleSlot>, Set<QuadTableForm>> answerKey() {
+		return answerKey;
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTriTable.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTriTable.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTriTable.java
new file mode 100644
index 0000000..fa09d83
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTriTable.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+public class TestTriTable extends TripleTableTest {
+
+	TriTable testTable = new TriTable();
+
+	@Override
+	protected TripleTable table() {
+		return testTable;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTripleTableForms.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTripleTableForms.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTripleTableForms.java
new file mode 100644
index 0000000..36cb652
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TestTripleTableForms.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static org.apache.jena.ext.com.google.common.collect.ImmutableSet.of;
+import static org.apache.jena.graph.Node.ANY;
+import static org.apache.jena.sparql.core.mem.TripleTableForm.*;
+import static org.apache.jena.sparql.core.mem.TupleSlot.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Triple;
+import org.junit.Test;
+
+public class TestTripleTableForms extends TupleTableFormsTest<TripleTableForm> {
+
+	@Override
+	public Stream<Set<TupleSlot>> queryPatterns() {
+		return TripleTableTest.tripleQueryPatterns();
+	}
+
+	@Override
+	protected Stream<TripleTableForm> tableForms() {
+		return TripleTableForm.tableForms();
+	}
+
+	@Test
+	public void addAndRemoveSomeTriples() {
+		tableForms().map(TripleTableForm::get).map(table -> new TripleTableTest() {
+
+			@Override
+			protected TripleTable table() {
+				return table;
+			}
+
+			@Override
+			protected Stream<Triple> tuples() {
+				return table.find(ANY, ANY, ANY);
+			}
+		}).forEach(TupleTableTest::addAndRemoveSomeTuples);
+	}
+
+	@Override
+	protected TripleTableForm chooseFrom(final Set<TupleSlot> sample) {
+		return TripleTableForm.chooseFrom(sample);
+	}
+
+	private final Map<Set<TupleSlot>, Set<TripleTableForm>> answerKey = new HashMap<Set<TupleSlot>, Set<TripleTableForm>>() {
+		{
+			put(of(SUBJECT), of(SPO));
+			put(of(PREDICATE), of(POS));
+			put(of(SUBJECT, PREDICATE), of(SPO));
+			put(of(OBJECT), of(OSP));
+			put(of(SUBJECT, OBJECT), of(OSP));
+			put(of(PREDICATE, OBJECT), of(POS));
+			put(of(SUBJECT, PREDICATE, OBJECT), of(SPO));
+			put(of(), of(SPO));
+		}
+	};
+
+	@Override
+	protected Map<Set<TupleSlot>, Set<TripleTableForm>> answerKey() {
+		return answerKey;
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TripleTableTest.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TripleTableTest.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TripleTableTest.java
new file mode 100644
index 0000000..9882466
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TripleTableTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static org.apache.jena.graph.Node.ANY;
+import static org.apache.jena.graph.NodeFactory.createURI;
+import static org.apache.jena.sparql.core.mem.QuadTableTest.quadQueryPatterns;
+import static org.apache.jena.sparql.core.mem.TupleSlot.GRAPH;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+public abstract class TripleTableTest extends TupleTableTest<Triple, TripleTable> {
+
+	private static final Node sampleNode = createURI("info:test");
+
+	private static final Triple testTriple = Triple.create(sampleNode, sampleNode, sampleNode);
+
+	@Override
+	protected Triple testTuple() {
+		return testTriple;
+	}
+
+	@Override
+	protected Stream<Triple> tuples() {
+		return table().find(ANY, ANY, ANY);
+	}
+
+	@Override
+	public Stream<Set<TupleSlot>> queryPatterns() {
+		return tripleQueryPatterns();
+	}
+
+	static Stream<Set<TupleSlot>> tripleQueryPatterns() {
+		return quadQueryPatterns().filter(s -> !s.contains(GRAPH));
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableFormsTest.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableFormsTest.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableFormsTest.java
new file mode 100644
index 0000000..2cc2d3a
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableFormsTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static java.util.stream.Collectors.toSet;
+import static org.apache.jena.sparql.core.mem.TupleTableTest.allWildcardQuery;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public abstract class TupleTableFormsTest<TableForm extends Predicate<Set<TupleSlot>>> extends Assert {
+
+	protected abstract Stream<TableForm> tableForms();
+
+	protected abstract Stream<Set<TupleSlot>> queryPatterns();
+
+	@Test
+	public void anAllWildcardQueryCannotAvoidTraversal() {
+		assertTrue(tableForms().noneMatch(form -> form.test(allWildcardQuery)));
+	}
+
+	@Test
+	public void anyIndexCanAnswerAnEntirelyConcretePattern() {
+		tableForms().allMatch(form -> form.test(allWildcardQuery));
+	}
+
+	protected boolean canAvoidTraversal(final Set<TupleSlot> pattern) {
+		return tableForms().anyMatch(form -> form.test(pattern));
+	}
+
+	@Test
+	public void allQueriesWithAtLeastOneConcreteNodeCanAvoidTraversal() {
+		assertTrue(queryPatterns().filter(p -> !allWildcardQuery.equals(p)).allMatch(this::canAvoidTraversal));
+	}
+
+	protected void avoidsTraversal(final Predicate<Set<TupleSlot>> indexForm,
+			final Set<Set<TupleSlot>> correctAnswers) {
+		final Set<Set<TupleSlot>> answers = queryPatterns().filter(indexForm::test).collect(toSet());
+		assertEquals(correctAnswers, answers);
+	}
+
+	@Test
+	public void aCorrectIndexIsChosenForEachPattern() {
+		answerKey().forEach((sample, correctAnswers) -> {
+			assertTrue(correctAnswers.contains(chooseFrom(sample)));
+		});
+	}
+
+	protected abstract TableForm chooseFrom(Set<TupleSlot> sample);
+
+	protected abstract Map<Set<TupleSlot>, Set<TableForm>> answerKey();
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableTest.java
----------------------------------------------------------------------
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableTest.java b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableTest.java
new file mode 100644
index 0000000..965f366
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/core/mem/TupleTableTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.sparql.core.mem;
+
+import static java.util.stream.Collectors.toSet;
+import static org.apache.jena.ext.com.google.common.collect.ImmutableSet.of;
+import static org.apache.jena.query.ReadWrite.READ;
+import static org.apache.jena.query.ReadWrite.WRITE;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.jena.ext.com.google.common.collect.ImmutableSet;
+import org.junit.Assert;
+import org.junit.Test;
+
+public abstract class TupleTableTest<TupleType, TupleTableType extends TupleTable<TupleType>> extends Assert {
+
+	protected abstract TupleType testTuple();
+
+	protected abstract TupleTableType table();
+
+	protected abstract Stream<TupleType> tuples();
+
+	protected abstract Stream<Set<TupleSlot>> queryPatterns();
+
+	protected static final Set<TupleSlot> allWildcardQuery = of();
+
+	@Test
+	public void addAndRemoveSomeTuples() {
+
+		// simple add-and-delete
+		table().begin(WRITE);
+		assertTrue(table().isInTransaction());
+		table().add(testTuple());
+		Set<TupleType> contents = tuples().collect(toSet());
+		assertEquals(ImmutableSet.of(testTuple()), contents);
+		table().delete(testTuple());
+		contents = tuples().collect(toSet());
+		assertTrue(contents.isEmpty());
+		table().end();
+		assertFalse(table().isInTransaction());
+
+		// add, abort, then check to see that nothing was persisted
+		table().begin(WRITE);
+		assertTrue(table().isInTransaction());
+		table().add(testTuple());
+		contents = tuples().collect(toSet());
+		assertEquals(ImmutableSet.of(testTuple()), contents);
+		table().abort();
+		assertFalse(table().isInTransaction());
+		table().begin(READ);
+		assertTrue(table().isInTransaction());
+		try {
+			contents = tuples().collect(toSet());
+			assertTrue(contents.isEmpty());
+		} finally {
+			table().end();
+			assertFalse(table().isInTransaction());
+		}
+
+		// add, commit, and check to see that persistence occurred
+		table().begin(WRITE);
+		assertTrue(table().isInTransaction());
+		table().add(testTuple());
+		contents = tuples().collect(toSet());
+		assertEquals(ImmutableSet.of(testTuple()), contents);
+		table().commit();
+		assertFalse(table().isInTransaction());
+		table().begin(READ);
+		assertTrue(table().isInTransaction());
+		try {
+			contents = tuples().collect(toSet());
+			assertEquals(ImmutableSet.of(testTuple()), contents);
+		} finally {
+			table().end();
+			assertFalse(table().isInTransaction());
+		}
+		// remove the test tuple and check to see that it is gone
+		table().begin(WRITE);
+		assertTrue(table().isInTransaction());
+		table().clear();
+		contents = tuples().collect(toSet());
+		assertTrue(contents.isEmpty());
+		table().commit();
+		table().begin(READ);
+		assertTrue(table().isInTransaction());
+		try {
+			contents = tuples().collect(toSet());
+			assertTrue(contents.isEmpty());
+		} finally {
+			table().end();
+			assertFalse(table().isInTransaction());
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/pom.xml
----------------------------------------------------------------------
diff --git a/jena-base/pom.xml b/jena-base/pom.xml
index b1c14d7..0ae90d4 100644
--- a/jena-base/pom.xml
+++ b/jena-base/pom.xml
@@ -51,7 +51,13 @@
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-csv</artifactId>
     </dependency> 
-
+    
+    <!-- supports persistent data structures -->
+    <dependency>
+      <groupId>com.github.andrewoma.dexx</groupId>
+      <artifactId>dexx-collections</artifactId>
+    </dependency>
+    
     <!-- Remove when jena-base is used -->
     <dependency>
       <groupId>org.apache.commons</groupId>

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PMap.java
----------------------------------------------------------------------
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PMap.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PMap.java
new file mode 100644
index 0000000..dba7160
--- /dev/null
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PMap.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import com.github.andrewoma.dexx.collection.Map;
+import com.github.andrewoma.dexx.collection.Maps;
+
+/**
+ * An implementation of {@link PersistentMap} using {@link com.github.andrewoma.dexx.collection.Map}.
+ *
+ * @param <K> the type of keys in the map
+ * @param <V> the type of values in this map
+ * @param <SelfType> the self-type of implementing classes
+ */
+public abstract class PMap<K, V, SelfType extends PMap<K, V, SelfType>> implements PersistentMap<K, V, SelfType> {
+
+	private final Map<K, V> wrappedMap;
+
+	private Map<K, V> wrappedMap() {
+		return wrappedMap;
+	}
+
+	/**
+	 * @param wrappedMap
+	 */
+	protected PMap(final Map<K, V> wrappedMap) {
+		this.wrappedMap = wrappedMap;
+	}
+
+	protected PMap() {
+		this(Maps.of());
+	}
+
+	/**
+	 * @param wrapped a map that supplies the internal state to be used
+	 * @return a new {@code SelfType} that holds the supplied internal state
+	 */
+	abstract protected SelfType wrap(final Map<K, V> wrapped);
+
+	@Override
+	public SelfType plus(final K key, final V value) {
+		return wrap(wrappedMap().put(key, value));
+	}
+
+	@Override
+	public SelfType minus(final K key) {
+		return wrap(wrappedMap().remove(key));
+	}
+
+	@Override
+	public Optional<V> get(final K key) {
+		return Optional.ofNullable(wrappedMap().get(key));
+	}
+
+	@Override
+	public boolean containsKey(final K key) {
+		return wrappedMap().containsKey(key);
+	}
+
+	@Override
+	public Stream<Entry<K, V>> entryStream() {
+		return wrappedMap().asMap().entrySet().stream();
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PSet.java
----------------------------------------------------------------------
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PSet.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PSet.java
new file mode 100644
index 0000000..1653c71
--- /dev/null
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PSet.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import java.util.stream.Stream;
+
+import com.github.andrewoma.dexx.collection.Set;
+import com.github.andrewoma.dexx.collection.Sets;
+
+/**
+ * A implementation of {@link PersistentSet} using {@link Set}.
+ *
+ * @param <E> the type of element in this set
+ */
+public class PSet<E> implements PersistentSet<E> {
+
+	private final Set<E> wrappedSet;
+
+	/**
+	 * @param wrappedSet
+	 */
+	private PSet(final Set<E> w) {
+		this.wrappedSet = w;
+	}
+
+	/**
+	 * @return an empty set
+	 */
+	public static <E> PSet<E> empty() {
+		return new PSet<>(Sets.of());
+	}
+
+	@Override
+	public PersistentSet<E> plus(final E e) {
+		return new PSet<>(wrappedSet.add(e));
+	}
+
+	@Override
+	public PersistentSet<E> minus(final E e) {
+		return new PSet<>(wrappedSet.remove(e));
+	}
+
+	@Override
+	public boolean contains(final E e) {
+		return wrappedSet.contains(e);
+	}
+
+	@Override
+	public Stream<E> stream() {
+		return wrappedSet.asSet().stream();
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentMap.java
----------------------------------------------------------------------
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentMap.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentMap.java
new file mode 100644
index 0000000..d3f25e3
--- /dev/null
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentMap.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+/**
+ * A persistent map data structure.
+ *
+ * @param <K> the type of keys in the map
+ * @param <V> the type of values in this map
+ * @param <SelfType> the self-type of implementing classes
+ */
+public interface PersistentMap<K, V, SelfType extends PersistentMap<K, V, SelfType>> {
+
+	/**
+	 * @param key
+	 * @return the value indexed by {@code key} if it exists
+	 */
+	default Optional<V> get(final K key) {
+		return entryStream().filter(e -> e.getKey().equals(key)).map(Entry::getValue).findFirst();
+	}
+
+	/**
+	 * @param key
+	 * @param value
+	 * @return a new {@code SelfType} with a new mapping from {@code key} to {@code value}
+	 */
+	SelfType plus(K key, V value);
+
+	/**
+	 * @param key
+	 * @return a new {@code SelfType} without the mapping indexed by {@code key}
+	 */
+	SelfType minus(K key);
+
+	/**
+	 * @param key
+	 * @return whether this map contains an entry indexed by {@code key}
+	 */
+	default boolean containsKey(final K key) {
+		return get(key).isPresent();
+	}
+
+	/**
+	 * @return a {@link Stream} of map entries
+	 */
+	Stream<Map.Entry<K, V>> entryStream();
+
+	/**
+	 * Sends this map's entries through a flattening function.
+	 *
+	 * @param f a function that flattens one entry into a stream
+	 * @return a stream of flattened entries
+	 */
+	default <R> Stream<R> flatten(final BiFunction<K, V, Stream<R>> f) {
+		return entryStream().flatMap(e -> f.apply(e.getKey(), e.getValue()));
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentSet.java
----------------------------------------------------------------------
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentSet.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentSet.java
new file mode 100644
index 0000000..92e799b
--- /dev/null
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/PersistentSet.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import java.util.stream.Stream;
+
+/**
+ * A persistent set data structure.
+ *
+ * @param <E> the type of element in this set
+ */
+public interface PersistentSet<E> {
+
+	/**
+	 * @return an empty set
+	 */
+	static <T> PersistentSet<T> empty() {
+		return PSet.empty();
+	}
+
+	/**
+	 * @param e an element
+	 * @return a new set with the elements of this set and {@code e}
+	 */
+	PersistentSet<E> plus(E e);
+
+	/**
+	 * @param e an element
+	 * @return a new set with the elements of this set except {@code e}
+	 */
+	PersistentSet<E> minus(E e);
+
+	/**
+	 * @param e an element
+	 * @return whether this set contains {@code e}
+	 */
+	boolean contains(E e);
+
+	/**
+	 * @return a {@link Stream} of the elements in this set
+	 */
+	Stream<E> stream();
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/package-info.java
----------------------------------------------------------------------
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/package-info.java b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/package-info.java
new file mode 100644
index 0000000..89f9820
--- /dev/null
+++ b/jena-base/src/main/java/org/apache/jena/atlas/lib/persistent/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Types for managing <a href="https://en.wikipedia.org/wiki/Persistent_data_structure">persistent</a> data structures.
+ */
+package org.apache.jena.atlas.lib.persistent;
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/test/java/org/apache/jena/atlas/TC_Atlas.java
----------------------------------------------------------------------
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/TC_Atlas.java b/jena-base/src/test/java/org/apache/jena/atlas/TC_Atlas.java
index f455207..cbe2b33 100644
--- a/jena-base/src/test/java/org/apache/jena/atlas/TC_Atlas.java
+++ b/jena-base/src/test/java/org/apache/jena/atlas/TC_Atlas.java
@@ -21,6 +21,7 @@ package org.apache.jena.atlas;
 import org.apache.jena.atlas.io.TS_IO ;
 import org.apache.jena.atlas.iterator.TS_Iterator ;
 import org.apache.jena.atlas.lib.TS_Lib ;
+import org.apache.jena.atlas.lib.persistent.TS_Persistent;
 import org.junit.runner.RunWith ;
 import org.junit.runners.Suite ;
 
@@ -30,12 +31,13 @@ import org.junit.runners.Suite ;
       TS_Lib.class
     , TS_Iterator.class
     , TS_IO.class
+    , TS_Persistent.class
 //    , TS_Event.class
 //    , TS_JSON.class
 //    , TS_Data.class
 //    , TS_Web.class
 //    , TestCSVParser.class
-}) 
+})
 
 public class TC_Atlas
 {}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TS_Persistent.java
----------------------------------------------------------------------
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TS_Persistent.java b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TS_Persistent.java
new file mode 100644
index 0000000..88e27ba
--- /dev/null
+++ b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TS_Persistent.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({ TestPSet.class, TestPMap.class })
+public class TS_Persistent {}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPMap.java
----------------------------------------------------------------------
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPMap.java b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPMap.java
new file mode 100644
index 0000000..ad6b9b8
--- /dev/null
+++ b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPMap.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import static java.util.stream.Collectors.toMap;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestPMap extends Assert {
+
+	@Test
+	public void containsAddedElement() {
+		final TestMap testMap = new TestMap();
+		assertFalse(testMap.containsKey("key"));
+		final TestMap nextMap = testMap.plus("key", "value");
+		assertTrue(nextMap.containsKey("key"));
+		assertEquals(Optional.of("value"), nextMap.get("key"));
+		final TestMap nextNextMap = nextMap.minus("key");
+		assertFalse(nextNextMap.containsKey("key"));
+	}
+
+	@Test
+	public void streaming() {
+		TestMap testMap = new TestMap().plus("key1", "value1").plus("key2", "value2");
+		final Stream<Entry<String, String>> testStream = testMap.entryStream();
+		final Map<String, String> recoveredMap = testStream.collect(toMap(Entry::getKey, Entry::getValue));
+		for (final Entry<String, String> e : recoveredMap.entrySet()) {
+			assertEquals(e.getValue(), testMap.get(e.getKey()).get());
+			testMap = testMap.minus(e.getKey());
+		}
+		assertEquals(0, testMap.entryStream().count());
+	}
+
+	private static class TestMap extends PMap<String, String, TestMap> {
+
+		/**
+		 * @param wrappedMap
+		 */
+		TestMap(final com.github.andrewoma.dexx.collection.Map<String, String> wrappedMap) {
+			super(wrappedMap);
+		}
+
+		TestMap() {
+			super();
+		}
+
+		@Override
+		protected TestMap wrap(final com.github.andrewoma.dexx.collection.Map<String, String> wrapped) {
+			return new TestMap(wrapped);
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPSet.java
----------------------------------------------------------------------
diff --git a/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPSet.java b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPSet.java
new file mode 100644
index 0000000..ae495d6
--- /dev/null
+++ b/jena-base/src/test/java/org/apache/jena/atlas/lib/persistent/TestPSet.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.atlas.lib.persistent;
+
+import static java.util.stream.Collectors.toSet;
+import static org.apache.jena.ext.com.google.common.collect.Sets.newHashSet;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestPSet extends Assert {
+
+	@Test
+	public void plusAndMinusWorkCorrectly() {
+		final Object testObject = new Object();
+		final PersistentSet<Object> testSet = PSet.empty();
+		assertFalse(testSet.contains(testObject));
+		final PersistentSet<Object> nextSet = testSet.plus(testObject);
+		assertTrue(nextSet.contains(testObject));
+		final PersistentSet<Object> nextNextSet = nextSet.minus(testObject);
+		assertFalse(nextNextSet.contains(testObject));
+	}
+
+	@Test
+	public void streaming() {
+		final Object testObject1 = new Object();
+		final Object testObject2 = new Object();
+		final PersistentSet<Object> testSet = PSet.empty().plus(testObject1).plus(testObject2);
+		assertEquals(newHashSet(testObject1, testObject2), testSet.stream().collect(toSet()));
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/DEPENDENCIES
----------------------------------------------------------------------
diff --git a/jena-core/DEPENDENCIES b/jena-core/DEPENDENCIES
index 4efc3ec..5ba2e6c 100644
--- a/jena-core/DEPENDENCIES
+++ b/jena-core/DEPENDENCIES
@@ -12,3 +12,6 @@ SLF4J : http://www.slf4j.org/
 
 JUnit : http://junit.org/
   Common Public License - v 1.0
+  
+Awaitility : https://github.com/jayway/awaitility/
+  Apache Software License - v 2.0

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/pom.xml
----------------------------------------------------------------------
diff --git a/jena-core/pom.xml b/jena-core/pom.xml
index da4d565..213e0eb 100644
--- a/jena-core/pom.xml
+++ b/jena-core/pom.xml
@@ -92,6 +92,12 @@
       <artifactId>mockito-all</artifactId>
       <scope>test</scope>
     </dependency>
+    
+    <dependency>
+      <groupId>com.jayway.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
 
     <dependency>
       <groupId>org.apache.jena</groupId>

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/src/main/java/org/apache/jena/assembler/JA.java
----------------------------------------------------------------------
diff --git a/jena-core/src/main/java/org/apache/jena/assembler/JA.java b/jena-core/src/main/java/org/apache/jena/assembler/JA.java
index 50df7d2..3005933 100644
--- a/jena-core/src/main/java/org/apache/jena/assembler/JA.java
+++ b/jena-core/src/main/java/org/apache/jena/assembler/JA.java
@@ -18,25 +18,31 @@
 
 package org.apache.jena.assembler;
 
-import org.apache.jena.rdf.model.* ;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.rdf.model.Property;
+import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.ResourceFactory;
 
 public class JA
     {
     public static final String uri = "http://jena.hpl.hp.com/2005/11/Assembler#";
-    
+
     public static String getURI()
         { return uri; }
 
     protected static Model schema;
-    
-    protected static Resource resource( String localName )
+
+    protected static Resource resource( final String localName )
         { return ResourceFactory.createResource( uri + localName ); }
-    
-    public static Property property( String localName )
+
+    public static Property property( final String localName )
         { return ResourceFactory.createProperty( uri + localName ); }
-    
+
     public static final Resource MemoryModel = resource( "MemoryModel" );
-    
+
+    public static final Resource MemoryDataset = resource( "MemoryDataset" );
+
     public static final Resource DefaultModel = resource( "DefaultModel" );
 
     public static final Resource InfModel = resource( "InfModel" );
@@ -46,11 +52,11 @@ public class JA
     public static final Property reasoner = property( "reasoner" );
 
     public static final Property reasonerURL = property( "reasonerURL" );
-    
+
     public static final Property baseModel = property( "baseModel" );
 
     public static final Property literalContent = property( "literalContent" );
-    
+
     public static final Property rules = property( "rules" );
 
     public static final Resource Model = resource( "Model" );
@@ -75,6 +81,8 @@ public class JA
 
     public static final Resource ModelSource = resource( "ModelSource" );
 
+    public static final Property data = property( "data" );
+
     public static final Property content = property( "content" );
 
     public static final Resource ExternalContent = resource( "ExternalContent" );
@@ -86,9 +94,9 @@ public class JA
     public static final Property ontModelSpec = property( "ontModelSpec" );
 
     public static final Resource This = resource( "this" );
-    
+
     public static final Resource True = resource( "true" );
-    
+
     public static final Resource False = resource( "false" );
 
     public static final Resource Expanded = resource( "Expanded" );
@@ -162,21 +170,21 @@ public class JA
     public static final Property fileEncoding = property( "fileEncoding" );
 
     public static final Property assembler = property( "assembler" );
-    
+
     public static final Property loadClass = property( "loadClass" );
-    
+
     public static final Property imports = property( "imports" );
 
     public static final Property reasonerFactory = property( "reasonerFactory" );
 
     public static final Property reasonerClass = property( "reasonerClass" );
-    
+
     public static final Property ja_schema = property( "schema" );
 
     public static final Property likeBuiltinSpec = property( "likeBuiltinSpec" );
 
     public static final Resource SinglePrefixMapping = resource( "SinglePrefixMapping");
-    
+
     public static final Property prefixMapping = property( "prefixMapping" );
 
     public static Model getSchema()
@@ -185,13 +193,13 @@ public class JA
         return schema;
         }
 
-    private static Model complete( Model m )
+    private static Model complete( final Model m )
         {
-        Model result = ModelFactory.createDefaultModel();
+        final Model result = ModelFactory.createDefaultModel();
         result.add( ModelFactory.createRDFSModel( m ) );
         return result;
         }
-    
+
     private static String getSchemaPath()
         { return "org/apache/jena/vocabulary/assembler.ttl"; }
     }

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/src/main/java/org/apache/jena/shared/LockMRPlusSW.java
----------------------------------------------------------------------
diff --git a/jena-core/src/main/java/org/apache/jena/shared/LockMRPlusSW.java b/jena-core/src/main/java/org/apache/jena/shared/LockMRPlusSW.java
new file mode 100644
index 0000000..9d73e39
--- /dev/null
+++ b/jena-core/src/main/java/org/apache/jena/shared/LockMRPlusSW.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.shared;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A multiple-reader plus single-writer lock. This lock permits readers to obtain their locks under any condition, but
+ * allows only one writer at a time. Writers can acquire a read lock, and readers can acquire the write lock.
+ *
+ */
+public class LockMRPlusSW extends ReentrantLock implements Lock {
+
+	@Override
+	public void enterCriticalSection(final boolean readLockRequested) {
+		if (!readLockRequested) lock();
+	}
+
+	@Override
+	public void leaveCriticalSection() {
+		if (isHeldByCurrentThread()) unlock();
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/src/test/java/org/apache/jena/shared/TestLockMRPlusSW.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/shared/TestLockMRPlusSW.java b/jena-core/src/test/java/org/apache/jena/shared/TestLockMRPlusSW.java
new file mode 100644
index 0000000..c91d979
--- /dev/null
+++ b/jena-core/src/test/java/org/apache/jena/shared/TestLockMRPlusSW.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.jena.shared;
+
+import static com.jayway.awaitility.Awaitility.await;
+import static java.lang.Thread.sleep;
+import static java.util.concurrent.Executors.defaultThreadFactory;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.jena.test.JenaTestBase;
+import org.junit.Test;
+
+import junit.framework.TestSuite;
+
+public class TestLockMRPlusSW extends JenaTestBase {
+
+	public TestLockMRPlusSW(final String name) {
+		super(name);
+	}
+
+	public static TestSuite suite() {
+		return new TestSuite(TestLockMRPlusSW.class);
+	}
+
+	@Test
+	public void testMultipleReadersAtATime() {
+		final Lock testLock = new LockMRPlusSW();
+		testLock.enterCriticalSection(true);
+		final AtomicBoolean secondReaderHasLock = new AtomicBoolean();
+		// new reader
+		defaultThreadFactory().newThread(() -> {
+			testLock.enterCriticalSection(true);
+			secondReaderHasLock.set(true);
+		}).start();
+		// the only way to fail is to timeout
+		await().untilTrue(secondReaderHasLock);
+	}
+
+	@Test
+	public void testOneWriterAtATime() throws InterruptedException {
+		final Lock testLock = new LockMRPlusSW();
+		testLock.enterCriticalSection(false);
+		final AtomicBoolean secondWriterHasLock = new AtomicBoolean();
+		// new writer
+		defaultThreadFactory().newThread(() -> {
+			testLock.enterCriticalSection(false);
+			secondWriterHasLock.set(true);
+		}).start();
+		sleep(5000);
+		assertFalse("Multiple writers were allowed!", secondWriterHasLock.get());
+	}
+
+	@Test
+	public void testAWriterDoesNotBlockReaders() {
+		final Lock testLock = new LockMRPlusSW();
+		testLock.enterCriticalSection(false);
+		final AtomicBoolean readerHasLock = new AtomicBoolean();
+		// new reader
+		defaultThreadFactory().newThread(() -> {
+			testLock.enterCriticalSection(true);
+			readerHasLock.set(true);
+		}).start();
+		await().untilTrue(readerHasLock);
+	}
+}

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-core/src/test/java/org/apache/jena/shared/TestSharedPackage.java
----------------------------------------------------------------------
diff --git a/jena-core/src/test/java/org/apache/jena/shared/TestSharedPackage.java b/jena-core/src/test/java/org/apache/jena/shared/TestSharedPackage.java
index 4f347f8..6bcd6f0 100644
--- a/jena-core/src/test/java/org/apache/jena/shared/TestSharedPackage.java
+++ b/jena-core/src/test/java/org/apache/jena/shared/TestSharedPackage.java
@@ -18,7 +18,8 @@
 
 package org.apache.jena.shared;
 
-import junit.framework.*;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
 
 public class TestSharedPackage extends TestCase
     {
@@ -26,10 +27,11 @@ public class TestSharedPackage extends TestCase
         { super(); }
 
     public static TestSuite suite()
-        { 
-        TestSuite result = new TestSuite();
+        {
+        final TestSuite result = new TestSuite();
         result.addTest( TestPrefixMapping.suite() );
         result.addTest( TestJenaException.suite() );
+        result.addTest( TestLockMRPlusSW.suite() );
         return result;
         }
     }

http://git-wip-us.apache.org/repos/asf/jena/blob/61054a21/jena-parent/pom.xml
----------------------------------------------------------------------
diff --git a/jena-parent/pom.xml b/jena-parent/pom.xml
index 0144565..924a180 100644
--- a/jena-parent/pom.xml
+++ b/jena-parent/pom.xml
@@ -191,6 +191,13 @@
         <version>4.0</version>
       </dependency>
       
+      <!-- supports persistent data structures -->
+      <dependency>
+        <groupId>com.github.andrewoma.dexx</groupId>
+        <artifactId>dexx-collections</artifactId>
+        <version>0.2</version>
+      </dependency>
+      
       <dependency>
         <groupId>com.github.jsonld-java</groupId>
         <artifactId>jsonld-java</artifactId>
@@ -354,6 +361,11 @@
         <version>1.9.5</version>
         <scope>test</scope>
       </dependency>
+      <dependency>
+        <groupId>com.jayway.awaitility</groupId>
+        <artifactId>awaitility</artifactId>
+        <version>1.6.4</version>
+      </dependency>
     </dependencies>
 
   </dependencyManagement>


Mime
View raw message