This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new d3fa1e9fd8 GH-3507: RDFS testing/wrapping framework. Fixed 
literals-in-subject inferences.
d3fa1e9fd8 is described below

commit d3fa1e9fd86af9931715b1c720f26f4479e9efa7
Author: Claus Stadler <[email protected]>
AuthorDate: Fri Aug 8 14:47:47 2025 +0200

    GH-3507: RDFS testing/wrapping framework. Fixed literals-in-subject 
inferences.
---
 .gitignore                                         |   4 +
 jena-arq/pom.xml                                   |  19 +-
 .../org/apache/jena/rdfs/DatasetGraphRDFS.java     | 120 +-----------
 .../main/java/org/apache/jena/rdfs/GraphRDFS.java  |  53 +----
 .../java/org/apache/jena/rdfs/RDFSFactory.java     |  18 +-
 .../jena/rdfs/assembler/DatasetRDFSAssembler.java  |   6 +-
 .../jena/rdfs/assembler/GraphRDFSAssembler.java    |   2 +-
 .../org/apache/jena/rdfs/engine/ApplyRDFS.java     |  10 +-
 .../DatasetGraphWithGraphTransform.java}           |  26 ++-
 .../org/apache/jena/rdfs/engine/GraphIncRDFS.java  |   9 +-
 .../{GraphRDFS.java => engine/GraphMatch.java}     |  37 ++--
 .../org/apache/jena/rdfs/engine/InfFindQuad.java   |   6 -
 .../org/apache/jena/rdfs/engine/InfFindTriple.java |   5 -
 .../java/org/apache/jena/rdfs/engine/MapperX.java  |   6 +
 .../java/org/apache/jena/rdfs/engine/Mappers.java  |   5 +
 .../java/org/apache/jena/rdfs/engine/Match.java    |  15 ++
 .../org/apache/jena/rdfs/engine/MatchAdapter.java  |  72 +++++++
 .../engine/{InfFindTriple.java => MatchGraph.java} |  37 ++--
 .../org/apache/jena/rdfs/engine/MatchRDFS.java     |  11 +-
 .../engine/{Match.java => MatchRDFSWrapper.java}   |  29 ++-
 .../rdfs/engine/{Match.java => MatchWrapper.java}  |  29 ++-
 .../org/apache/jena/rdfs/setup/BaseSetupRDFS.java  |   2 +-
 .../org/apache/jena/rdfs/setup/MatchVocabRDFS.java |   7 +
 .../jena/rdfs/AbstractDatasetGraphCompare.java     | 217 +++++++++++++++++++++
 .../apache/jena/rdfs/AbstractTestRDFS_Extra.java   |  85 ++++++++
 .../apache/jena/rdfs/AbstractTestRDFS_Find.java    | 127 ++++++++++++
 .../org/apache/jena/rdfs/GraphFindExecutable.java  |  99 ++++++++++
 .../test/java/org/apache/jena/rdfs/TS_InfRdfs.java |   2 +
 .../jena/rdfs/TestDatasetGraphFindRDFS.java}       |  26 ++-
 .../org/apache/jena/rdfs/TestDatasetGraphRDFS.java |   5 +-
 .../apache/jena/atlas/iterator/FilterUnique.java   |   1 -
 .../{FilterUnique.java => FilterUniqueCache.java}  |  19 +-
 .../java/org/apache/jena/atlas/iterator/Iter.java  |  13 ++
 .../apache/jena/atlas/iterator/IteratorConcat.java |   4 +-
 .../org/apache/jena/tdb2/match/MapperXTDB.java     |  57 ++++++
 .../java/org/apache/jena/tdb2/match/MatchTDB.java  |  74 +++++++
 .../org/apache/jena/tdb2/match/TestMatchTDB2.java  |  82 ++++++++
 pom.xml                                            |   9 +-
 38 files changed, 1072 insertions(+), 276 deletions(-)

diff --git a/.gitignore b/.gitignore
index e93f7bff2a..7e29af4163 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,7 @@ hs_err_*
 
 # Fuseki file area
 run/
+
+# Benchmark JSON results
+jena-benchmarks/jena-benchmarks-jmh/*.json
+
diff --git a/jena-arq/pom.xml b/jena-arq/pom.xml
index 2939e959ab..a1698678ce 100644
--- a/jena-arq/pom.xml
+++ b/jena-arq/pom.xml
@@ -45,7 +45,7 @@
     <dependency>
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
-    </dependency>    
+    </dependency>
 
     <dependency>
       <groupId>org.slf4j</groupId>
@@ -71,12 +71,12 @@
       <artifactId>jakarta.json</artifactId>
     </dependency>
     <!-- End Titanium JSON-LD 1.1 -->
-    
+
     <dependency>
       <groupId>com.google.protobuf</groupId>
       <artifactId>protobuf-java</artifactId>
     </dependency>
-      
+
     <dependency>
       <groupId>org.apache.thrift</groupId>
       <artifactId>libthrift</artifactId>
@@ -124,6 +124,11 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-math4-legacy</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -139,7 +144,7 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-resources-plugin</artifactId>
       </plugin>
-      
+
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
@@ -172,11 +177,11 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-source-plugin</artifactId>
-        <executions> 
+        <executions>
           <execution>
-            <id>attach-sources-test</id> 
+            <id>attach-sources-test</id>
             <goals>
-              <goal>test-jar-no-fork</goal> 
+              <goal>test-jar-no-fork</goal>
             </goals>
           </execution>
         </executions>
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java
index 1ca880b9c8..765334920b 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java
@@ -18,134 +18,22 @@
 
 package org.apache.jena.rdfs;
 
-import static org.apache.jena.atlas.iterator.Iter.iter;
-
-import java.util.Iterator;
-
-import org.apache.jena.atlas.iterator.Iter;
-import org.apache.jena.graph.Graph;
-import org.apache.jena.graph.Node;
+import org.apache.jena.rdfs.engine.DatasetGraphWithGraphTransform;
 import org.apache.jena.sparql.core.DatasetGraph;
-import org.apache.jena.sparql.core.DatasetGraphWrapper;
 import org.apache.jena.sparql.core.DatasetGraphWrapperView;
-import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.util.Context;
 
-public class DatasetGraphRDFS extends DatasetGraphWrapper implements 
DatasetGraphWrapperView {
+public class DatasetGraphRDFS extends DatasetGraphWithGraphTransform 
implements DatasetGraphWrapperView {
     // Do not unwrap for query execution.
-
     private final SetupRDFS setup;
 
     public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup) {
-        super(dsg);
+        super(dsg, g -> new GraphRDFS(g, setup));
         this.setup = setup;
     }
 
     public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup, Context cxt) {
-        super(dsg, cxt);
+        super(dsg, cxt, g -> new GraphRDFS(g, setup));
         this.setup = setup;
     }
-
-    // Graph-centric access.
-    @Override
-    public Graph getDefaultGraph() {
-        Graph base = getG().getDefaultGraph();
-        return new GraphRDFS(base, setup);
-    }
-
-    @Override
-    public Graph getUnionGraph() {
-        Graph base = getG().getUnionGraph();
-        return new GraphRDFS(base, setup);
-    }
-
-    @Override
-    public Graph getGraph(Node graphNode) {
-        Graph base = getG().getGraph(graphNode);
-        if ( base == null )
-            return null;
-        return new GraphRDFS(base, setup);
-    }
-
-    @Override
-    public Iterator<Quad> find()
-    { return find(Node.ANY, Node.ANY, Node.ANY, Node.ANY); }
-
-    // Quad-centric access
-    @Override
-    public Iterator<Quad> find(Quad quad) {
-        return find(quad.getGraph(), quad.getSubject(), quad.getPredicate(), 
quad.getObject());
-    }
-
-    @Override
-    public Iterator<Quad> find(Node g, Node s, Node p, Node o) {
-        Iterator<Quad> iter = findInf(g, s, p, o);
-        if ( iter == null )
-            return Iter.nullIterator();
-        return iter;
-    }
-
-//    private Iterator<Quad> findInf(Node g, Node s, Node p, Node o) {
-//        // Puts in the graph name for the quad base don g even if g is ANY 
or null.
-//        MatchRDFS<Node, Quad> infMatcher = new InfFindQuad(setup, g, getR());
-//        Stream<Quad> quads = infMatcher.match(s, p, o);
-//        Iterator<Quad> iter = quads.iterator();
-//        iter = Iter.onClose(iter, ()->quads.close());
-//        return iter;
-//    }
-
-    /**
-     * Find, graph by graph.
-     */
-    private Iterator<Quad> findInf(Node g, Node s, Node p, Node o) {
-        if ( g != null && g.isConcrete() ) {
-            // Includes the union graph case.
-            return findOneGraphInf(g, s, p, o);
-        }
-        // Wildcard. Do each graph in-term.
-        // This ensures the graph node of the quad corresponds to where the 
inference came from.
-        Iter<Quad> iter1 = findOneGraphInf(Quad.defaultGraphIRI, s, p, o);
-        Iterator<Quad> iter2 = findAllNamedGraphInf(s, p, o);
-        return iter1.append(iter2);
-    }
-
-    // All named graphs, with inference. Quads refer to the name graph they 
were caused by.
-    private Iterator<Quad> findAllNamedGraphInf(Node s, Node p, Node o) {
-        return Iter.flatMap(listGraphNodes(), gn -> findOneGraphInf(gn, s, p, 
o));
-    }
-
-    // Single graph (inc. union graph). Quads refer to the name graph they 
were caused by.
-    private Iter<Quad> findOneGraphInf(Node g, Node s, Node p, Node o) {
-        if ( ! g.isConcrete()  )
-            throw new IllegalStateException();
-        // f ( Quad.isUnionGraph(g) ) {}
-        // Specific named graph.
-        return iter(getGraph(g).find(s,p,o)).map(t->Quad.create(g, t));
-    }
-
-    @Override
-    public Iterator<Quad> findNG(Node g, Node s, Node p, Node o) {
-        if ( Quad.isDefaultGraph(g) )
-            throw new IllegalArgumentException("Default graph in findNG call");
-        if ( g == null )
-            g = Node.ANY;
-        if ( g == Node.ANY )
-            return findAllNamedGraphInf(s, p, o);
-        // Same as specific named graph - we return quads in the union graph.
-//        if ( Quad.isUnionGraph(g) ) {}
-        return findOneGraphInf(g, s, p, o);
-    }
-
-    @Override
-    public boolean contains(Quad quad)
-    { return contains(quad.getGraph(), quad.getSubject(), quad.getPredicate(), 
quad.getObject()); }
-
-    @Override
-    public boolean contains(Node g, Node s, Node p, Node o) {
-        // Go through the inference machinery.
-        Iterator<Quad> iter = find(g, s, p, o);
-        try {
-            return iter.hasNext();
-        } finally { Iter.close(iter); }
-    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java
index 0fdece6ad2..60139ec767 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java
@@ -18,66 +18,21 @@
 
 package org.apache.jena.rdfs;
 
-import java.util.stream.Stream;
-
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
-import org.apache.jena.graph.Triple;
+import org.apache.jena.rdfs.engine.GraphMatch;
 import org.apache.jena.rdfs.engine.InfFindTriple;
-import org.apache.jena.rdfs.engine.MatchRDFS;
 import org.apache.jena.rdfs.setup.ConfigRDFS;
-import org.apache.jena.sparql.graph.GraphWrapper;
-import org.apache.jena.util.iterator.ExtendedIterator;
-import org.apache.jena.util.iterator.WrappedIterator;
 
 /**
  * RDFS graph over a base graph.
  */
-public class GraphRDFS extends GraphWrapper {
-    private final MatchRDFS<Node, Triple> source;
+public class GraphRDFS extends GraphMatch {
     private final ConfigRDFS<Node> setup;
 
     public GraphRDFS(Graph graph, ConfigRDFS<Node> setup) {
-        super(graph);
+        super(graph, new InfFindTriple(setup, graph));
         this.setup = setup;
-        this.source = new InfFindTriple(setup, graph);
-    }
-
-    @Override
-    public ExtendedIterator<Triple> find(Triple m) {
-        return find(m.getSubject(), m.getPredicate(), m.getObject());
-    }
-
-    @Override
-    public ExtendedIterator<Triple> find(Node s, Node p, Node o) {
-        Stream<Triple> stream = source.match(s, p, o);
-        ExtendedIterator<Triple> iter = WrappedIterator.ofStream(stream);
-        return iter;
-    }
-
-    @Override
-    public Stream<Triple> stream(Node s, Node p, Node o) {
-        return source.match(s, p, o);
-    }
-
-    @Override
-    public boolean contains(Node s, Node p, Node o) {
-        // Must go via find()-like functionality.
-        ExtendedIterator<Triple> iter = find(s, p, o);
-        try {
-            return iter.hasNext();
-        } finally { iter.close(); }
-    }
-
-    @Override
-    public boolean contains(Triple t) {
-        return contains(t.getSubject(), t.getPredicate(), t.getObject());
-    }
-
-    @Override
-    public int size() {
-        // Report the size of the underlying graph.
-        // Even better, don't ask.
-        return super.size();
     }
 }
+
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
index 37e42fe723..034d73eb8a 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/RDFSFactory.java
@@ -18,8 +18,12 @@
 
 package org.apache.jena.rdfs;
 import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
 import org.apache.jena.query.Dataset;
 import org.apache.jena.query.DatasetFactory;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.rdfs.setup.BaseSetupRDFS;
+import org.apache.jena.rdfs.setup.ConfigRDFS;
 import org.apache.jena.riot.system.StreamRDF;
 import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.sparql.util.NodeUtils;
@@ -56,13 +60,13 @@ public class RDFSFactory {
     /** Create an RDFS inference dataset. */
     public static DatasetGraph datasetRDFS(DatasetGraph data, Graph vocab ) {
         SetupRDFS setup = setupRDFS(vocab);
-        return new DatasetGraphRDFS(data, setup);
+        return datasetRDFS(data, setup);
     }
 
     /** Create an RDFS inference dataset. */
     public static Dataset datasetRDFS(Dataset data, Graph vocab ) {
         SetupRDFS setup = setupRDFS(vocab);
-        return DatasetFactory.wrap(new DatasetGraphRDFS(data.asDatasetGraph(), 
setup));
+        return DatasetFactory.wrap(datasetRDFS(data.asDatasetGraph(), setup));
     }
 
     /** Create an {@link SetupRDFS} */
@@ -70,6 +74,16 @@ public class RDFSFactory {
         return new SetupRDFS(vocab);
     }
 
+    /** Create a {@link ConfigRDFS} via a {@link MapperX}. */
+    public static <X> ConfigRDFS<X> setupRDFS(Graph vocab, MapperX<X, ?> 
mapper) {
+        return new BaseSetupRDFS<>(vocab) {
+            @Override
+            protected X fromNode(Node node) {
+                return mapper.fromNode(node);
+            }
+        };
+    }
+
     /** Stream expand data based on a separate vocabulary */
     public static StreamRDF streamRDFS(StreamRDF data, Graph vocab) {
         SetupRDFS setup = new SetupRDFS(vocab);
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java
 
b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java
index f82ec6c80e..7ac921a05d 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/DatasetRDFSAssembler.java
@@ -26,7 +26,6 @@ import org.apache.jena.assembler.Assembler;
 import org.apache.jena.assembler.exceptions.AssemblerException;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.rdf.model.Resource;
-import org.apache.jena.rdfs.DatasetGraphRDFS;
 import org.apache.jena.rdfs.RDFSFactory;
 import org.apache.jena.rdfs.SetupRDFS;
 import org.apache.jena.riot.RDFDataMgr;
@@ -50,7 +49,7 @@ public class DatasetRDFSAssembler extends 
NamedDatasetAssembler {
     /**
      * <pre>
      * &lt;#rdfsDS&gt; rdf:type ja:DatasetRDFS ;
-     *      ja:rdfs "vocab.ttl";
+     *      ja:rdfsSchema "vocab.ttl";
      *      ja:dataset &lt;#baseDS&gt; ;
      *      .
      *
@@ -58,7 +57,6 @@ public class DatasetRDFSAssembler extends 
NamedDatasetAssembler {
      *     ja:name "TIM database"  # optional: this is need if the base 
database is accessed directly.
      *     ja:data "data1.trig";
      *     ## ja:data "data2.trig";
-     *
      *     .
      * </pre>
      */
@@ -76,7 +74,7 @@ public class DatasetRDFSAssembler extends 
NamedDatasetAssembler {
 
         Graph schema = RDFDataMgr.loadGraph(schemaFile);
         SetupRDFS setup = RDFSFactory.setupRDFS(schema);
-        DatasetGraph dsg = new DatasetGraphRDFS(base, setup);
+        DatasetGraph dsg = RDFSFactory.datasetRDFS(base, setup);
         AssemblerUtils.mergeContext(root, dsg.getContext());
         return dsg;
     }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java
index f0869d3190..8d1315bf33 100644
--- 
a/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/rdfs/assembler/GraphRDFSAssembler.java
@@ -48,7 +48,7 @@ public class GraphRDFSAssembler extends AssemblerBase 
implements Assembler {
     /**
      * <pre>
      * &lt;#rdfsGraph&gt; rdf:type ja:GraphRDFS ;
-     *      ja:rdfs "vocab.ttl";
+     *      ja:rdfsSchema "vocab.ttl";
      *      ja:graph &lt;#baseGraph&gt; ;
      *      .
      *
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
index 9d69662f51..69943e7515 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/ApplyRDFS.java
@@ -129,10 +129,12 @@ public class ApplyRDFS<X, T> extends CxtInf<X,T>{
                 return;
         }
         Set<X> x = setup.getRange(p);
-        x.forEach(c -> {
-            derive(o, rdfType, c, out);
-            subClass(o, rdfType, c, out);
-        });
+        if (!mapper.isLiteral(o)) {
+            x.forEach(c -> {
+                derive(o, rdfType, c, out);
+                subClass(o, rdfType, c, out);
+            });
+        }
     }
 }
 
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java
similarity index 86%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java
copy to 
jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java
index 1ca880b9c8..aacd9c5ac1 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java
+++ 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/DatasetGraphWithGraphTransform.java
@@ -16,11 +16,12 @@
  * limitations under the License.
  */
 
-package org.apache.jena.rdfs;
+package org.apache.jena.rdfs.engine;
 
 import static org.apache.jena.atlas.iterator.Iter.iter;
 
 import java.util.Iterator;
+import java.util.function.Function;
 
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.graph.Graph;
@@ -31,32 +32,37 @@ import org.apache.jena.sparql.core.DatasetGraphWrapperView;
 import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.util.Context;
 
-public class DatasetGraphRDFS extends DatasetGraphWrapper implements 
DatasetGraphWrapperView {
+public class DatasetGraphWithGraphTransform extends DatasetGraphWrapper 
implements DatasetGraphWrapperView {
     // Do not unwrap for query execution.
 
-    private final SetupRDFS setup;
+    private Function<Graph, ? extends Graph> graphTransform;
 
-    public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup) {
+    public DatasetGraphWithGraphTransform(DatasetGraph dsg, Function<Graph, ? 
extends Graph> graphTransform) {
         super(dsg);
-        this.setup = setup;
+        this.graphTransform = graphTransform;
     }
 
-    public DatasetGraphRDFS(DatasetGraph dsg, SetupRDFS setup, Context cxt) {
+    public DatasetGraphWithGraphTransform(DatasetGraph dsg, Context cxt, 
Function<Graph, ? extends Graph> graphTransform) {
         super(dsg, cxt);
-        this.setup = setup;
+        this.graphTransform = graphTransform;
+    }
+
+    private Graph wrapGraph(Graph graph) {
+        Graph result = graphTransform.apply(graph);
+        return result;
     }
 
     // Graph-centric access.
     @Override
     public Graph getDefaultGraph() {
         Graph base = getG().getDefaultGraph();
-        return new GraphRDFS(base, setup);
+        return wrapGraph(base);
     }
 
     @Override
     public Graph getUnionGraph() {
         Graph base = getG().getUnionGraph();
-        return new GraphRDFS(base, setup);
+        return wrapGraph(base);
     }
 
     @Override
@@ -64,7 +70,7 @@ public class DatasetGraphRDFS extends DatasetGraphWrapper 
implements DatasetGrap
         Graph base = getG().getGraph(graphNode);
         if ( base == null )
             return null;
-        return new GraphRDFS(base, setup);
+        return wrapGraph(base);
     }
 
     @Override
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
index 0fcfa02e2d..2eb3a4ea7a 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphIncRDFS.java
@@ -18,7 +18,8 @@
 
 package org.apache.jena.rdfs.engine;
 
-import static org.apache.jena.rdfs.engine.ConstRDFS.*;
+import static org.apache.jena.rdfs.engine.ConstRDFS.rdfType;
+import static org.apache.jena.rdfs.engine.ConstRDFS.rdfsSubClassOf;
 
 import java.util.Set;
 import java.util.stream.Stream;
@@ -53,7 +54,6 @@ public class GraphIncRDFS extends GraphRDFS {
                 
.filter(type->!setup.getSubClassHierarchy().keySet().contains(type))
                 .map(type->Triple.create(type, rdfsSubClassOf, type))
                 );
-
     }
 
     @Override
@@ -73,6 +73,11 @@ public class GraphIncRDFS extends GraphRDFS {
         return iter;
     }
 
+    @Override
+    public boolean contains(Node s, Node p, Node o) {
+        return vocab.contains(s, p, o) || super.contains(s, p, o);
+    }
+
     private Stream<Triple> extras(Node s, Node p, Node o) {
         return extra.stream().filter(t->
             matchTerm(t.getSubject(),s) && matchTerm(t.getPredicate(),p) && 
matchTerm(t.getObject(),o) );
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java
similarity index 70%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java
copy to jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java
index 0fdece6ad2..32b9e633e5 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/GraphRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/GraphMatch.java
@@ -16,31 +16,40 @@
  * limitations under the License.
  */
 
-package org.apache.jena.rdfs;
+package org.apache.jena.rdfs.engine;
 
 import java.util.stream.Stream;
 
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
-import org.apache.jena.rdfs.engine.InfFindTriple;
-import org.apache.jena.rdfs.engine.MatchRDFS;
-import org.apache.jena.rdfs.setup.ConfigRDFS;
 import org.apache.jena.sparql.graph.GraphWrapper;
 import org.apache.jena.util.iterator.ExtendedIterator;
 import org.apache.jena.util.iterator.WrappedIterator;
 
 /**
- * RDFS graph over a base graph.
+ * A Graph view over a {@link Match}. A graph can be specified as a delegate
+ * for all functionality that is not covered by the Match.
  */
-public class GraphRDFS extends GraphWrapper {
-    private final MatchRDFS<Node, Triple> source;
-    private final ConfigRDFS<Node> setup;
+public class GraphMatch extends GraphWrapper {
+    private final Match<Node, Triple> source;
 
-    public GraphRDFS(Graph graph, ConfigRDFS<Node> setup) {
+    public GraphMatch(Graph graph, Match<Node, Triple> match) {
         super(graph);
-        this.setup = setup;
-        this.source = new InfFindTriple(setup, graph);
+        this.source = match;
+    }
+
+    /**
+     * Wrap a base graph such that its find() and contains() methods
+     * are delegated to the match.
+     * Other methods, such as those for updates, go to the base graph.
+     */
+    public static <X, T> Graph adapt(Graph baseGraph, Match<X, T> match) {
+        return new GraphMatch(baseGraph, new MatchAdapter<>(match, 
match.getMapper()));
+    }
+
+    public Match<Node, Triple> getMatch() {
+        return source;
     }
 
     @Override
@@ -62,11 +71,7 @@ public class GraphRDFS extends GraphWrapper {
 
     @Override
     public boolean contains(Node s, Node p, Node o) {
-        // Must go via find()-like functionality.
-        ExtendedIterator<Triple> iter = find(s, p, o);
-        try {
-            return iter.hasNext();
-        } finally { iter.close(); }
+        return source.contains(s, p, o);
     }
 
     @Override
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
index c9a771bd39..97d86206e1 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindQuad.java
@@ -59,10 +59,4 @@ public class InfFindQuad extends MatchRDFS<Node, Quad> {
     protected boolean sourceContains(Node s, Node p, Node o) {
         return dsg.contains(graph, s, p, o);
     }
-
-    @Override
-    protected Quad dstCreate(Node s, Node p, Node o) {
-        // Must be concrete for this quad creation.
-        return Quad.create(graph, s, p, o);
-    }
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
index 9511acd0e1..2f152f3c3c 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
@@ -50,9 +50,4 @@ public class InfFindTriple extends MatchRDFS<Node, Triple> {
     protected boolean sourceContains(Node s, Node p, Node o) {
         return graph.contains(s, p, o);
     }
-
-    @Override
-    protected Triple dstCreate(Node s, Node p, Node o) {
-        return Triple.create(s, p, o);
-    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
index 29a3650e47..bf136cdbbb 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
@@ -28,4 +28,10 @@ public interface MapperX<X,T> {
     public abstract X subject(T tuple);
     public abstract X predicate(T tuple);
     public abstract X object(T tuple);
+
+    public abstract T tuple(X s, X p, X o);
+
+    public default boolean isLiteral(X x) {
+        return toNode(x).isLiteral();
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
index 5c70427c1e..2666cc0a7c 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Mappers.java
@@ -19,6 +19,8 @@
 package org.apache.jena.rdfs.engine;
 
 import org.apache.jena.atlas.lib.tuple.Tuple;
+import org.apache.jena.atlas.lib.tuple.Tuple3;
+import org.apache.jena.atlas.lib.tuple.TupleFactory;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.sparql.core.Quad;
@@ -42,6 +44,7 @@ public class Mappers {
         @Override public Node subject(Triple triple)    { return 
triple.getSubject(); }
         @Override public Node predicate(Triple triple)  { return 
triple.getPredicate(); }
         @Override public Node object(Triple triple)     { return 
triple.getObject(); }
+        @Override public Triple tuple(Node s, Node p, Node o) { return 
Triple.create(s, p, o); }
     }
 
     private static class MapperQuad implements MapperX<Node, Quad> {
@@ -52,6 +55,7 @@ public class Mappers {
         @Override public Node subject(Quad quad)    { return 
quad.getSubject(); }
         @Override public Node predicate(Quad quad)  { return 
quad.getPredicate(); }
         @Override public Node object(Quad quad)     { return quad.getObject(); 
}
+        @Override public Quad tuple(Node s, Node p, Node o) { return 
Quad.create(graph, s, p, o); }
     }
 
     private static class MapperTuple implements MapperX<Node, Tuple<Node>> {
@@ -60,6 +64,7 @@ public class Mappers {
         @Override public Node subject(Tuple<Node> tuple)    { return 
offset(tuple, 0); }
         @Override public Node predicate(Tuple<Node> tuple)  { return 
offset(tuple, 1); }
         @Override public Node object(Tuple<Node> tuple)     { return 
offset(tuple, 2); }
+        @Override public Tuple3<Node> tuple(Node s, Node p, Node o) { return 
TupleFactory.create3(s, p, o); }
 
         private static Node offset(Tuple<Node> tuple, int i) {
             int idx = ( tuple.len() == 3 ) ? i : i+1;
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
index 2fa7cf4151..32601b45c7 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
@@ -20,10 +20,25 @@ package org.apache.jena.rdfs.engine;
 
 import java.util.stream.Stream;
 
+import org.apache.jena.graph.Node;
+
 /**
  * Match by S/P/O where {@code X} is the RDF term representation (Node, 
NodeId) and
  * {@code T} is the tuple (triple, quad, tuple) representation.
  */
 public interface Match<X, T> {
     public Stream<T> match(X s, X p, X o);
+
+    public default boolean contains(X s, X p, X o) {
+        try (Stream<T> stream = match(s, p, o)) {
+            return stream.findFirst().isPresent();
+        }
+    }
+
+    /**
+     * The mapper for reuse with wrappers.
+     * Note that this indirectly ties the {@link Match} interface to the 
{@link Node} realm:
+     * One can use the mapper to obtain X for e.g. RDF.Nodes.type.
+     */
+    MapperX<X, T> getMapper();
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java
new file mode 100644
index 0000000000..0d4aaf5e79
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchAdapter.java
@@ -0,0 +1,72 @@
+/*
+ * 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.rdfs.engine;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.Triple;
+
+/**
+ * This is the bridge between the Node/Triple level and some lower level such 
as one based on NodeIds.
+ */
+public final class MatchAdapter<X, T>
+    implements Match<Node, Triple>
+{
+    private Match<X, T> below;
+    private MapperX<X, T> mapper;
+
+    public MatchAdapter(Match<X, T> below, MapperX<X, T> mapper) {
+        super();
+        this.mapper = Objects.requireNonNull(mapper);
+        this.below = Objects.requireNonNull(below);
+    }
+
+    @Override
+    public Stream<Triple> match(Node s, Node p, Node o) {
+        X sd = down(s);
+        X pd = down(p);
+        X od = down(o);
+        return below.match(sd, pd, od).map(this::up);
+    }
+
+    private X down(Node node) {
+        return mapper.fromNode(node);
+    }
+
+    private Triple up(T tuple) {
+        X sd = mapper.subject(tuple);
+        X pd = mapper.predicate(tuple);
+        X od = mapper.object(tuple);
+        Node s = mapper.toNode(sd);
+        Node p = mapper.toNode(pd);
+        Node o = mapper.toNode(od);
+        return dstCreate(s, p, o);
+    }
+
+    private Triple dstCreate(Node s, Node p, Node o) {
+        return Triple.create(s, p, o);
+    }
+
+    @Override
+    public MapperX<Node, Triple> getMapper() {
+        return Mappers.mapperTriple();
+    }
+}
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java
similarity index 56%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
copy to jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java
index 9511acd0e1..40d064eed5 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/InfFindTriple.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchGraph.java
@@ -18,41 +18,38 @@
 
 package org.apache.jena.rdfs.engine;
 
+import java.util.Objects;
 import java.util.stream.Stream;
 
-import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
-import org.apache.jena.rdfs.setup.ConfigRDFS;
-import org.apache.jena.util.iterator.ExtendedIterator;
 
 /**
- * Find in one graph.
+ * A {@link Match} view over a {@link Graph}.
+ * This class is final. Use {@link MatchWrapper} to modify match behavior.
  */
-public class InfFindTriple extends MatchRDFS<Node, Triple> {
-
-    private final Graph graph;
-
-    public InfFindTriple(ConfigRDFS<Node> setup, Graph graph) {
-        super(setup, Mappers.mapperTriple());
-        this.graph = graph;
+public final class MatchGraph
+    implements Match<Node, Triple>
+{
+    private Graph base;
+
+    public MatchGraph(Graph base) {
+        super();
+        this.base = Objects.requireNonNull(base);
     }
 
-    @Override
-    public Stream<Triple> sourceFind(Node s, Node p, Node o) {
-        ExtendedIterator<Triple> iter = graph.find(s,p,o);
-        Stream<Triple> stream = Iter.asStream(iter);
-        return stream;
+    public Graph getGraph() {
+        return base;
     }
 
     @Override
-    protected boolean sourceContains(Node s, Node p, Node o) {
-        return graph.contains(s, p, o);
+    public Stream<Triple> match(Node s, Node p, Node o) {
+        return base.stream(s, p, o);
     }
 
     @Override
-    protected Triple dstCreate(Node s, Node p, Node o) {
-        return Triple.create(s, p, o);
+    public MapperX<Node, Triple> getMapper() {
+        return Mappers.mapperTriple();
     }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
index cef8c1af9f..9ab430f61d 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFS.java
@@ -73,6 +73,11 @@ public abstract class MatchRDFS<X, T> extends CxtInf<X, T> 
implements Match<X,T>
         };
     }
 
+    @Override
+    public MapperX<X, T> getMapper() {
+        return mapper;
+    }
+
     @Override
     public final Stream<T> match(X s, X p, X o) { return matchWithInf(s, p 
,o); }
 
@@ -84,7 +89,6 @@ public abstract class MatchRDFS<X, T> extends CxtInf<X, T> 
implements Match<X,T>
     // Access data.
     protected abstract boolean sourceContains(X s, X p, X o);
     protected abstract Stream<T> sourceFind(X s, X p, X o);
-    protected abstract T dstCreate(X s, X p, X o);
 
     protected final X subject(T tuple)        { return mapper.subject(tuple); }
     protected final X predicate(T tuple)      { return 
mapper.predicate(tuple); }
@@ -460,6 +464,11 @@ public abstract class MatchRDFS<X, T> extends CxtInf<X, T> 
implements Match<X,T>
         return map == null || map.isEmpty();
     }
 
+    /** Inherit tuple construction from the mapper. */
+    protected T dstCreate(X s, X p, X o) {
+        return getMapper().tuple(s, p, o);
+    }
+
 //  private void print(Map<X, Set<X>> map) {
 //      System.out.println("{");
 //      CollectionUtils.forEach(map, (k,v)->System.out.printf("  %-20s  %s\n", 
k, v));
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java
similarity index 57%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
copy to jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java
index 2fa7cf4151..eee372f35e 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchRDFSWrapper.java
@@ -18,12 +18,29 @@
 
 package org.apache.jena.rdfs.engine;
 
+import java.util.Objects;
 import java.util.stream.Stream;
 
-/**
- * Match by S/P/O where {@code X} is the RDF term representation (Node, 
NodeId) and
- * {@code T} is the tuple (triple, quad, tuple) representation.
- */
-public interface Match<X, T> {
-    public Stream<T> match(X s, X p, X o);
+import org.apache.jena.rdfs.setup.ConfigRDFS;
+
+/** MatchRDFS implementation as a wrapper over another Match source. */
+public class MatchRDFSWrapper<X, T>
+    extends MatchRDFS<X, T>
+{
+    protected Match<X, T> source;
+
+    public MatchRDFSWrapper(ConfigRDFS<X> setup, Match<X, T> source) {
+        super(setup, source.getMapper());
+        this.source = Objects.requireNonNull(source);
+    }
+
+    @Override
+    public Stream<T> sourceFind(X s, X p, X o) {
+        return source.match(s,p,o);
+    }
+
+    @Override
+    protected boolean sourceContains(X s, X p, X o) {
+        return source.contains(s, p, o);
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java
similarity index 64%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
copy to jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java
index 2fa7cf4151..b36f43f979 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/Match.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MatchWrapper.java
@@ -20,10 +20,27 @@ package org.apache.jena.rdfs.engine;
 
 import java.util.stream.Stream;
 
-/**
- * Match by S/P/O where {@code X} is the RDF term representation (Node, 
NodeId) and
- * {@code T} is the tuple (triple, quad, tuple) representation.
- */
-public interface Match<X, T> {
-    public Stream<T> match(X s, X p, X o);
+public class MatchWrapper<X, T, D extends Match<X, T>>
+    implements Match<X, T>
+{
+    protected D delegate;
+
+    public MatchWrapper(D delegate) {
+        super();
+        this.delegate = delegate;
+    }
+
+    public D getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public Stream<T> match(X s, X p, X o) {
+        return getDelegate().match(s, p, o);
+    }
+
+    @Override
+    public MapperX<X, T> getMapper() {
+        return getDelegate().getMapper();
+    }
 }
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
index 56d94426fa..81e5408638 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/BaseSetupRDFS.java
@@ -308,7 +308,7 @@ public abstract class BaseSetupRDFS<X> implements 
ConfigRDFS<X>{
 
     private static <X> void put(Map<X, Set<X>> multimap, X n1, X n2) {
         if ( !multimap.containsKey(n1) )
-            multimap.put(n1, new HashSet<X>());
+            multimap.put(n1, new HashSet<>());
         multimap.get(n1).add(n2);
     }
 
diff --git 
a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java 
b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
index 7e3eb4ac8b..18bf0a083d 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
+++ b/jena-arq/src/main/java/org/apache/jena/rdfs/setup/MatchVocabRDFS.java
@@ -31,6 +31,8 @@ import java.util.stream.Stream;
 
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.rdfs.engine.Mappers;
 import org.apache.jena.rdfs.engine.Match;
 
 /**
@@ -112,4 +114,9 @@ public class MatchVocabRDFS implements Match<Node, Triple>{
                 .stream()
                 .flatMap( 
e->e.getValue().stream().map(obj->Triple.create(e.getKey(), p, obj)) );
     }
+
+    @Override
+    public MapperX<Node, Triple> getMapper() {
+        return Mappers.mapperTriple();
+    }
 }
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java
new file mode 100644
index 0000000000..dc21b41c5e
--- /dev/null
+++ 
b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractDatasetGraphCompare.java
@@ -0,0 +1,217 @@
+/*
+ * 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.rdfs;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.stream.IntStreams;
+import org.apache.commons.numbers.combinatorics.Combinations;
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+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.graph.NodeTransformLib;
+import org.junit.jupiter.api.DynamicTest;
+
+/**
+ * Test to check consistency of the find() method.
+ *
+ * Base class for generating tests that invoke the find() method of a dataset 
graph
+ * with all combinations of patterns.
+ */
+public abstract class AbstractDatasetGraphCompare {
+    private String testLabel;
+
+    public AbstractDatasetGraphCompare(String testLabel) {
+        super();
+        this.testLabel = testLabel;
+    }
+
+    public String getTestLabel() {
+        return testLabel;
+    }
+
+    /** By default consider cardinality when evaluating result sets. */
+    protected boolean defaultCompareAsSet() {
+        return true;
+    }
+
+
+    /** Sub classes can use this method to generate dynamic tests. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph referenceDsg, 
DatasetGraph testDsg, DatasetGraph dataDsg) {
+        List<Quad> findQuads = createFindQuads(dataDsg).toList();
+        return new GraphFindTestBuilder(testLabel, referenceDsg, testDsg, 
findQuads)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    /** Derive a reference dataset by materalizing the dataset into a copy. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph testDsg, 
DatasetGraph dataDsg) {
+        DatasetGraph referenceDsg = DatasetGraphFactory.create();
+        referenceDsg.addAll(testDsg);
+
+        List<Quad> findQuads = createFindQuads(dataDsg).toList();
+        return new GraphFindTestBuilder(testLabel, referenceDsg, testDsg, 
findQuads)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    /** Materialize the dataset as the reference dataset. Use reference 
dataset as dataDsg. */
+    public GraphFindTestBuilder prepareFindTests(DatasetGraph testDsg) {
+        DatasetGraph referenceDsg = DatasetGraphFactory.create();
+        referenceDsg.addAll(testDsg);
+        return prepareFindTests(testDsg, referenceDsg, testDsg)
+            .compareAsSet(defaultCompareAsSet());
+    }
+
+    // Markers for find-quad generation w.r.t. a dataset and a
+    // "meta pattern" such as (IN, foo, OUT, OUT).
+    // IN becomes substituted with concrete values, out becomes ANY.
+    private static final Node IN = NodeFactory.createBlankNode("IN");
+    private static final Node OUT = NodeFactory.createBlankNode("OUT");
+
+    /**
+     * Generate the set of find patterns for each quad in the source dataset.
+     * This is the set of combinations by substituting components with ANY.
+     * For example, the derivations for a concrete quad (g, s, p, o) are:
+     * <pre>
+     * {(g, s, p, ANY), (g, s, ANY, o), (g, s, ANY, ANY), ...}
+     * </pre>
+     */
+    public static Stream<Quad> createFindQuads(DatasetGraph dataSource) {
+        Node[] baseMetaPattern = new Node[]{OUT, OUT, OUT, OUT};
+        Stream<Quad> result = IntStream.rangeClosed(0, 4).boxed()
+            .flatMap(k -> Iter.asStream(Combinations.of(4, k).iterator()))
+            .flatMap(ins -> {
+                Node[] metaPattern = Arrays.copyOf(baseMetaPattern, 
baseMetaPattern.length);
+
+                // Use IN to mark the components that we want to substitute 
with concrete values.
+                // OUT becomes ANY.
+                IntStreams.of(ins).forEach(i -> metaPattern[i] = IN);
+                Quad metaQuad = toQuad(List.of(metaPattern).iterator());
+
+                Set<Quad> lookups = createFindQuads(dataSource, metaQuad);
+                return lookups.stream();
+            });
+        return result;
+    }
+
+    private static Quad toQuad(Iterator<Node> it) {
+        Quad r = Quad.create(it.next(), it.next(), it.next(), it.next());
+        if (it.hasNext()) {
+            throw new IllegalArgumentException("Iterator of exactly 4 elements 
expected.");
+        }
+        return r;
+    }
+
+    private static Node outToAny(Node pattern, Node concrete) {
+        Node r = (OUT.equals(pattern)) ? Node.ANY : concrete;
+        return r;
+    }
+
+    private static Node inToAny(Node node) {
+        Node r = (IN.equals(node)) ? Node.ANY : node;
+        return r;
+    }
+
+    private static Node outToAny(Node node) {
+        Node r = (OUT.equals(node)) ? Node.ANY : node;
+        return r;
+    }
+
+    private static Quad inToAny(Quad metaQuad) {
+        return 
NodeTransformLib.transform(AbstractDatasetGraphCompare::inToAny, metaQuad);
+    }
+
+    private static Quad outToAny(Quad metaQuad) {
+        return 
NodeTransformLib.transform(AbstractDatasetGraphCompare::outToAny, metaQuad);
+    }
+
+    // !!! This method implicitly gets rid of 'IN' !!!
+    // Components of the input quads are processed as follows:
+    // If a component of pattern is OUT then it becomes is ANY.
+    // Otherwise, *always* return the corresponding component of 'concrete'.
+    private static Quad createFindQuad(Quad meta, Quad concrete) {
+        Quad result = Quad.create(
+            outToAny(meta.getGraph(), concrete.getGraph()),
+            outToAny(meta.getSubject(), concrete.getSubject()),
+            outToAny(meta.getPredicate(), concrete.getPredicate()),
+            outToAny(meta.getObject(), concrete.getObject()));
+        return result;
+    }
+
+    /**
+     * Expand a pattern such as (IN, s, OUT, OUT) into { (g1, s, ANY, ANY), 
(g2, s, ANY, ANY) }
+     * based on the concrete quads in dsg.
+     */
+    private static Set<Quad> createFindQuads(DatasetGraph dsg, Quad metaQuad) {
+        // Replace IN and OUT with ANY - this retains only term nodes.
+        Quad p = outToAny(inToAny(metaQuad));
+        Set<Quad> result = Iter.collect(Iter.map(dsg.find(p), q -> 
createFindQuad(metaQuad, q)),
+                Collectors.toCollection(LinkedHashSet::new));
+        return result;
+    }
+
+    /**
+     * Builder that accepts a reference dataset, a test dataset
+     * and a list of quads for which to produce {@link DynamicTest} instances.
+     *
+     * <p>
+     * In addition, it allows to configure whether result sets should be 
compared as sets instead of lists.
+     * Useful to assess correctness in disregard of cardinality.
+     */
+    public static class GraphFindTestBuilder {
+        private String testLabel;
+        private DatasetGraph referenceDsg;
+        private DatasetGraph testDsg;
+        private List<Quad> findQuads;
+        private boolean compareAsSet;
+
+        protected GraphFindTestBuilder(String testLabel, DatasetGraph 
referenceDsg, DatasetGraph testDsg, List<Quad> findQuads) {
+            super();
+            this.testLabel = Objects.requireNonNull(testLabel);
+            this.referenceDsg = Objects.requireNonNull(referenceDsg);
+            this.testDsg = Objects.requireNonNull(testDsg);
+            this.findQuads = findQuads;
+        }
+
+        public List<Quad> getFindQuads() {
+            return findQuads;
+        }
+
+        public GraphFindTestBuilder compareAsSet(boolean onOrOff) {
+            this.compareAsSet = onOrOff;
+            return this;
+        }
+
+        public List<DynamicTest> build() {
+            List<DynamicTest> tests = findQuads.stream().map(q -> 
DynamicTest.dynamicTest(testLabel + " " + q,
+                new GraphFindExecutable(testLabel, q, referenceDsg, testDsg, 
compareAsSet))).toList();
+            return tests;
+        }
+    }
+}
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java
new file mode 100644
index 0000000000..116e50553a
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Extra.java
@@ -0,0 +1,85 @@
+/*
+ * 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.rdfs;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+
+public abstract class AbstractTestRDFS_Extra
+    extends AbstractTestRDFS_Find
+{
+    public AbstractTestRDFS_Extra(String testLabel) {
+        super(testLabel);
+    }
+
+    /** Some RDFS reasoners so far produce duplicates which fail cardinality 
tests. */
+    @Override
+    protected boolean defaultCompareAsSet() {
+        return true;
+    }
+
+    @TestFactory
+    @Disabled("Fails on certain patterns - Needs investigation.")
+    public List<DynamicTest> testSubPropertyOfRdfType01() {
+        List<DynamicTest> tests = prepareRdfsFindTestsSSE(
+            "(graph (:directType rdfs:subPropertyOf rdf:type) )",
+            "(graph (:fido :directType :Dog) )"
+        ).build();
+        return tests;
+    }
+
+    @TestFactory
+    public List<DynamicTest> testSubClassOf01() {
+        List<DynamicTest> tests = prepareRdfsFindTestsSSE(
+            "(graph (:Dog rdfs:subClassOf rdf:Mammal) )",
+            "(graph (:fido rdf:type :Dog) )"
+        ).build();
+        return tests;
+    }
+
+    @TestFactory
+    public List<DynamicTest> testRange01() {
+        List<DynamicTest> tests = prepareRdfsFindTestsSSE(
+            "(graph (:owner rdfs:range :Person) )",
+            "(graph (:fido :owner :alice) )"
+        ).build();
+        return tests;
+    }
+
+    @TestFactory
+    public List<DynamicTest> testRangeWithLiteral01() {
+        List<DynamicTest> tests = prepareRdfsFindTestsSSE(
+            "(graph (:name rdfs:range :Literal) )",
+            "(graph (:fido :name 'Fido') )"
+        ).build();
+        return tests;
+    }
+
+    @TestFactory
+    public List<DynamicTest> testDomain01() {
+        List<DynamicTest> tests = prepareRdfsFindTestsSSE(
+            "(graph (:owner rdfs:domain :Pet) )",
+            "(graph (:fido :owner :alice) )"
+        ).build();
+        return tests;
+    }
+}
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java
new file mode 100644
index 0000000000..21e0d9c015
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/AbstractTestRDFS_Find.java
@@ -0,0 +1,127 @@
+/*
+ * 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.rdfs;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Node;
+import org.apache.jena.rdfs.setup.ConfigRDFS;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.RDFDataMgr;
+import org.apache.jena.riot.RDFFormat;
+import org.apache.jena.riot.RDFParser;
+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.sse.SSE;
+import org.apache.jena.vocabulary.RDF;
+import org.apache.jena.vocabulary.RDFS;
+
+/**
+ * Test consistency of DatasetGraph.find(g, s, p, o) w.r.t. an RDFS setup.
+ * <p>
+ *
+ * <b>BEWARE:</b> find() is used to produce the reference data. Errors in the 
reference data
+ * will likely cause otherwise correctly functioning tests to fail.
+ * <p>
+ *
+ * (1) Materializes the result of dsg.find() into a reference dataset.
+ * (2) Invokes find(g, s, p, o) with all combinations and compares the results.
+ */
+public abstract class AbstractTestRDFS_Find
+    extends AbstractDatasetGraphCompare
+{
+    public AbstractTestRDFS_Find(String testLabel) {
+        super(testLabel);
+    }
+
+    /** Some RDFS reasoners so far produce duplicates which fail cardinality 
tests. */
+    @Override
+    protected boolean defaultCompareAsSet() {
+        return true;
+    }
+
+    /** Sub classes need to implement this method and return a DatasetGraph 
with RDFS inferencing. */
+    protected abstract DatasetGraph applyRdfs(DatasetGraph dsg, 
ConfigRDFS<Node> configRDFS);
+
+    /**
+     * Prepare test cases.
+     *
+     * @param schemaStr SSE expression that parses as a {@link Graph}.
+     * @param dataStr SSE expression that parses as a {@link DatasetGraph}.
+     * @return A builder for the concrete test instances.
+     */
+    public GraphFindTestBuilder prepareRdfsFindTestsSSE(String schemaStr, 
String dataStr) {
+        Graph graph = SSE.parseGraph(schemaStr);
+        DatasetGraph inputDsg = SSE.parseDatasetGraph(dataStr);
+        return prepareRdfsFindTests(graph, inputDsg);
+    }
+
+    /**
+     * Prepare test cases.
+     *
+     * @param schemaStr RDF data in TRIG syntax that parses as a {@link Graph}.
+     * @param dataStr RDF data in TRIG syntax that parses as a {@link 
DatasetGraph}.
+     * @return A builder for the concrete test instances.
+     */
+    public GraphFindTestBuilder prepareRdfsFindTestsTrig(String schemaStr, 
String dataStr) {
+        Graph schemaGraph = RDFParser.fromString(schemaStr, 
Lang.TTL).toGraph();
+        SetupRDFS setup = RDFSFactory.setupRDFS(schemaGraph);
+
+        DatasetGraph inputDsg = RDFParser.fromString(dataStr, 
Lang.TRIG).toDatasetGraph();
+        return prepareRdfsFindTests(setup, inputDsg);
+    }
+
+    public GraphFindTestBuilder prepareRdfsFindTests(Graph schemaGraph, 
DatasetGraph inputDsg) {
+        SetupRDFS setup = RDFSFactory.setupRDFS(schemaGraph);
+        return prepareRdfsFindTests(setup, inputDsg);
+    }
+
+    public GraphFindTestBuilder prepareRdfsFindTests(ConfigRDFS<Node> 
configRDFS, DatasetGraph inputDsg) {
+        DatasetGraph testDsg = applyRdfs(inputDsg, configRDFS);
+
+        // Build reference data by materializing a copy of testDsg via 
findAll().
+        DatasetGraph referenceDsg = DatasetGraphFactory.create();
+        referenceDsg.prefixes().putAll(testDsg.prefixes());
+        referenceDsg.addAll(testDsg);
+
+        // dataDsg that is the source for substituting placeholders
+        // in the quads that will be used to test find(quad) calls.
+        DatasetGraph dataDsg = DatasetGraphFactory.create();
+        dataDsg.prefixes().putAll(referenceDsg.prefixes());
+        dataDsg.addAll(referenceDsg);
+
+        boolean debugPrintReferenceData = false;
+        if (debugPrintReferenceData) {
+            RDFDataMgr.write(System.out, referenceDsg, RDFFormat.TRIG_PRETTY);
+        }
+
+        // Add all (non-literal) objects to the source Data for more extensive 
testing of lookups.
+        try (Stream<Quad> stream = dataDsg.stream()) {
+            List<Quad> extra = stream.flatMap(q -> Stream.of(q.getPredicate(), 
q.getObject())
+                    .map(x -> Quad.create(q.getGraph(), x, RDF.Nodes.type, 
RDFS.Nodes.Resource)))
+                    .toList();
+            extra.forEach(dataDsg::add);
+        }
+
+        return prepareFindTests(referenceDsg, testDsg, referenceDsg);
+    }
+}
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java
new file mode 100644
index 0000000000..304ceebd90
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/GraphFindExecutable.java
@@ -0,0 +1,99 @@
+/*
+ * 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.rdfs;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.atlas.lib.ListUtils;
+import org.apache.jena.riot.out.NodeFmtLib;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.Quad;
+import org.junit.jupiter.api.function.Executable;
+
+/**
+ * JUnit test case executable to compare two datasets w.r.t.
+ * their result of find(pattern).
+ */
+public class GraphFindExecutable
+    implements Executable
+{
+    private static PrintStream out = System.err;
+
+    private String       testLabel;
+    private Quad         findPattern;
+    private DatasetGraph referenceDsg;
+    private DatasetGraph testDsg;
+
+    /** Compare expected and actual data as sets instead of lists. */
+    private boolean compareAsSet;
+
+    public GraphFindExecutable(String testLabel, Quad findPattern, 
DatasetGraph referenceDsg, DatasetGraph testDsg, boolean compareAsSet) {
+        super();
+        this.testLabel    = testLabel;
+        this.referenceDsg = referenceDsg;
+        this.testDsg = testDsg;
+        this.findPattern = findPattern;
+        this.compareAsSet = compareAsSet;
+    }
+
+    public String getTestLabel() {
+        return testLabel;
+    }
+
+    public Quad getFindPattern() {
+        return findPattern;
+    }
+
+    /**
+     * Assert that graph.find() returned the same set of quads for the given 
pattern.
+     * Duplicates are ignored.
+     */
+    @Override
+    public void execute() throws Throwable {
+        List<Quad> expectedList = Iter.toList(referenceDsg.find(findPattern));
+        List<Quad> actualList = Iter.toList(testDsg.find(findPattern));
+
+        if (compareAsSet) {
+            Set<Quad> expectedSet = new LinkedHashSet<>(expectedList);
+            Set<Quad> actualSet = new LinkedHashSet<>(actualList);
+            expectedList = new ArrayList<>(expectedSet);
+            actualList = new ArrayList<>(actualSet);
+        }
+
+        boolean b = ListUtils.equalsUnordered(expectedList, actualList);
+        if ( ! b ) {
+            out.println("Fail: find(" + NodeFmtLib.str(findPattern) + ")");
+            LibTestRDFS.printDiff(out, expectedList, actualList);
+        }
+
+        assertTrue(b,()->getTestLabel());
+    }
+
+    @Override
+    public String toString() {
+        return getTestLabel() + " " + getFindPattern();
+    }
+}
diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java
index 83419bd6f0..6a957a1597 100644
--- a/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TS_InfRdfs.java
@@ -51,6 +51,8 @@ import org.junit.platform.suite.api.Suite;
     , TestInfSPARQL.class
 
     , TestAssemblerRDFS.class
+
+    , TestDatasetGraphFindRDFS.class
 })
 
 public class TS_InfRdfs { }
diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java
similarity index 61%
copy from jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
copy to 
jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java
index 29a3650e47..716c403ae3 100644
--- a/jena-arq/src/main/java/org/apache/jena/rdfs/engine/MapperX.java
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphFindRDFS.java
@@ -16,16 +16,26 @@
  * limitations under the License.
  */
 
-package org.apache.jena.rdfs.engine;
+package org.apache.jena.rdfs;
 
 import org.apache.jena.graph.Node;
+import org.apache.jena.rdfs.setup.ConfigRDFS;
+import org.apache.jena.sparql.core.DatasetGraph;
 
-/** Bridge between Node and X; 3-tuples and Triple/Quad */
-public interface MapperX<X,T> {
-    public abstract X fromNode(Node n);
-    public abstract Node toNode(X x);
+public class TestDatasetGraphFindRDFS
+    extends AbstractTestRDFS_Extra
+{
+     public TestDatasetGraphFindRDFS() {
+        super("RDFS");
+     }
 
-    public abstract X subject(T tuple);
-    public abstract X predicate(T tuple);
-    public abstract X object(T tuple);
+     @Override
+     protected boolean defaultCompareAsSet() {
+         return true;
+     }
+
+     @Override
+     protected DatasetGraph applyRdfs(DatasetGraph dsg, ConfigRDFS<Node> 
configRDFS) {
+         return new DatasetGraphRDFS(dsg, (SetupRDFS)configRDFS);
+     }
 }
diff --git 
a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java 
b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java
index 6588923d2c..10a9231362 100644
--- a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java
+++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java
@@ -42,6 +42,7 @@ import org.apache.jena.atlas.lib.StrUtils;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphWrapper;
 import org.apache.jena.sparql.core.Quad;
 import org.apache.jena.sparql.sse.SSE;
 
@@ -54,7 +55,7 @@ import org.apache.jena.sparql.sse.SSE;
 public class TestDatasetGraphRDFS {
 
     private static PrintStream out = System.out;
-    private static DatasetGraphRDFS dsg;
+    private static DatasetGraphWrapper dsg;
 
     @BeforeAll
     public static void beforeClass() {
@@ -66,7 +67,7 @@ public class TestDatasetGraphRDFS {
                 );
         DatasetGraph dsgBase = SSE.parseDatasetGraph(x);
         Graph schema = SSE.parseGraph("(graph (:A rdfs:subClassOf :B))");
-        dsg = (DatasetGraphRDFS)RDFSFactory.datasetRDFS(dsgBase, schema);
+        dsg = (DatasetGraphWrapper)RDFSFactory.datasetRDFS(dsgBase, schema);
     }
 
     @Test public void dsg_access_1() {
diff --git 
a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java 
b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java
index 96dfa5e029..b9015a7e88 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java
@@ -29,5 +29,4 @@ public class FilterUnique<T> implements Predicate<T> {
     public boolean test(T item) {
         return seen.add(item);
     }
-
 }
diff --git 
a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java 
b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java
similarity index 69%
copy from 
jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java
copy to 
jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java
index 96dfa5e029..886865a4be 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUnique.java
+++ 
b/jena-base/src/main/java/org/apache/jena/atlas/iterator/FilterUniqueCache.java
@@ -18,16 +18,23 @@
 
 package org.apache.jena.atlas.iterator;
 
-import java.util.HashSet;
-import java.util.Set;
 import java.util.function.Predicate;
 
-public class FilterUnique<T> implements Predicate<T> {
-    private final Set<T> seen = new HashSet<>();
+import org.apache.jena.atlas.lib.Cache;
+import org.apache.jena.atlas.lib.CacheFactory;
+
+public class FilterUniqueCache<T> implements Predicate<T> {
+    private final Cache<T, Object> seen;
+
+    public FilterUniqueCache(int size) {
+        super();
+        this.seen = CacheFactory.createCache(size);
+    }
 
     @Override
     public boolean test(T item) {
-        return seen.add(item);
+        boolean wasSeen = seen.containsKey(item);
+        seen.put(item, Boolean.TRUE);
+        return !wasSeen;
     }
-
 }
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java 
b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
index e8ab211939..17da693695 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/Iter.java
@@ -27,6 +27,7 @@ import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
 import org.apache.jena.atlas.io.IO;
+import org.apache.jena.atlas.lib.CacheFactory;
 import org.apache.jena.atlas.lib.Closeable;
 import org.apache.jena.atlas.lib.Sink;
 
@@ -107,6 +108,10 @@ public class Iter<T> implements IteratorCloseable<T> {
         return t == null ? Iter.empty() : Iter.of(t);
     }
 
+    public static <T> Iterator<T> ofStream(Stream<T> stream) {
+        return Iter.onClose(stream.iterator(), stream::close);
+    }
+
     /**
      * Return an iterator that does not permit remove.
      * This makes an "UnmodifiableIterator".
@@ -518,6 +523,14 @@ public class Iter<T> implements IteratorCloseable<T> {
         return filter(iter, new FilterUnique<T>());
     }
 
+    /** Returns an iterator that uses an LRU cache to filter out duplicates.
+     * This provides a best-effort distinct view within a sliding window of 
recent elements.
+     * Memory usage is bounded by the specified cache size.
+     */
+    public static <T> Iterator<T> distinctCached(Iterator<T> iter, int 
cacheMaxSize) {
+        return filter(iter, new FilterUniqueCache<T>(cacheMaxSize));
+    }
+
     /** Remove adjacent duplicates. This operation does not need
      * working memory to remember the all elements already seen,
      * just a slot for the last element seen.
diff --git 
a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java 
b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
index 04b82cfd9c..5d25163968 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/iterator/IteratorConcat.java
@@ -114,11 +114,11 @@ public class IteratorConcat<T> implements 
IteratorCloseable<T> {
     @Override
     public void close() {
         //iterators.forEach(Iter::close);
-        // Earlier iterators already closed
+        // Earlier iterators already closed. Handle case where hasNext has 
never been called.
+        if (idx == -1) idx = 0;
         for ( int i = idx ; i < iterators.size() ; i++ ) {
             Iterator<T> iter = iterators.get(idx);
             Iter.close(iter);
         }
-
     }
 }
diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java
new file mode 100644
index 0000000000..067520f05c
--- /dev/null
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MapperXTDB.java
@@ -0,0 +1,57 @@
+/*
+ * 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.tdb2.match;
+
+import org.apache.jena.atlas.lib.tuple.Tuple3;
+import org.apache.jena.atlas.lib.tuple.TupleFactory;
+import org.apache.jena.graph.Node;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.tdb2.store.DatasetGraphTDB;
+import org.apache.jena.tdb2.store.NodeId;
+import org.apache.jena.tdb2.store.nodetable.NodeTable;
+import org.apache.jena.tdb2.sys.TDBInternal;
+
+public class MapperXTDB
+    implements MapperX<NodeId, Tuple3<NodeId>>
+{
+    private NodeTable nodeTable;
+
+    protected MapperXTDB(NodeTable nodeTable) {
+        super();
+        this.nodeTable = nodeTable;
+    }
+
+    public static MapperX<NodeId, Tuple3<NodeId>> create(DatasetGraph dsg) {
+        DatasetGraphTDB tdb = TDBInternal.getDatasetGraphTDB(dsg);
+        if (tdb == null) {
+            throw new IllegalArgumentException("Argument must be a TDB2 
dataset graph.");
+        }
+        return new 
MapperXTDB(tdb.getQuadTable().getNodeTupleTable().getNodeTable());
+    }
+
+    @Override public NodeId fromNode(Node n)   { return 
nodeTable.getNodeIdForNode(n); }
+    @Override public Node   toNode  (NodeId x) { return 
nodeTable.getNodeForNodeId(x); }
+
+    @Override public NodeId subject  (Tuple3<NodeId> tuple) { return 
tuple.get(0); }
+    @Override public NodeId predicate(Tuple3<NodeId> tuple) { return 
tuple.get(1); }
+    @Override public NodeId object   (Tuple3<NodeId> tuple) { return 
tuple.get(2); }
+
+    @Override public Tuple3<NodeId> tuple(NodeId s, NodeId p, NodeId o) { 
return TupleFactory.create3(s, p, o); }
+}
diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.java 
b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.java
new file mode 100644
index 0000000000..2aac132b69
--- /dev/null
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/match/MatchTDB.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.tdb2.match;
+
+import java.util.stream.Stream;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.atlas.lib.tuple.Tuple3;
+import org.apache.jena.atlas.lib.tuple.TupleFactory;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.rdfs.engine.Match;
+import org.apache.jena.tdb2.store.GraphTDB;
+import org.apache.jena.tdb2.store.GraphViewSwitchable;
+import org.apache.jena.tdb2.store.NodeId;
+import org.apache.jena.tdb2.store.nodetable.NodeTable;
+
+public class MatchTDB
+    implements Match<NodeId, Tuple3<NodeId>>
+{
+    private GraphTDB graph;
+    private MapperX<NodeId, Tuple3<NodeId>> mapper;
+
+    protected MatchTDB(GraphTDB graph, MapperX<NodeId, Tuple3<NodeId>> mapper) 
{
+        super();
+        this.graph = graph;
+        this.mapper = mapper;
+    }
+
+    public static MatchTDB wrap(Graph g) {
+        // Same pattern used in stage generator - move to TDBInternal?
+        if ( g instanceof GraphViewSwitchable gvs )
+            g = gvs.getBaseGraph();
+
+        if (!(g instanceof GraphTDB tdbGraph)) {
+            throw new IllegalArgumentException("Not a TDB2 graph");
+        }
+
+        NodeTable nodeTable = tdbGraph.getNodeTupleTable().getNodeTable();
+        MapperX<NodeId, Tuple3<NodeId>> mapper = new MapperXTDB(nodeTable);
+        return new MatchTDB(tdbGraph, mapper);
+    }
+
+    @Override
+    public Stream<Tuple3<NodeId>> match(NodeId s, NodeId p, NodeId o) {
+        return Iter.asStream(graph.getNodeTupleTable().find(s, p, o))
+            .filter(t -> {
+                boolean b = NodeId.isDoesNotExist(t.get(0)) || 
NodeId.isDoesNotExist(t.get(1)) || NodeId.isDoesNotExist(t.get(2));
+                return !b;
+            })
+            .map(t -> TupleFactory.create3(t.get(0), t.get(1), t.get(2)));
+    }
+
+    @Override
+    public MapperX<NodeId, Tuple3<NodeId>> getMapper() {
+        return mapper;
+    }
+}
diff --git 
a/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.java 
b/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.java
new file mode 100644
index 0000000000..a77091a55a
--- /dev/null
+++ b/jena-tdb2/src/test/java/org/apache/jena/tdb2/match/TestMatchTDB2.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.tdb2.match;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Set;
+
+import org.apache.jena.atlas.iterator.Iter;
+import org.apache.jena.atlas.lib.tuple.Tuple3;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.query.ReadWrite;
+import org.apache.jena.rdfs.RDFSFactory;
+import org.apache.jena.rdfs.engine.DatasetGraphWithGraphTransform;
+import org.apache.jena.rdfs.engine.GraphMatch;
+import org.apache.jena.rdfs.engine.MapperX;
+import org.apache.jena.rdfs.engine.MatchRDFSWrapper;
+import org.apache.jena.rdfs.setup.ConfigRDFS;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.sse.SSE;
+import org.apache.jena.system.AutoTxn;
+import org.apache.jena.system.G;
+import org.apache.jena.system.Txn;
+import org.apache.jena.tdb2.TDB2Factory;
+import org.apache.jena.tdb2.store.NodeId;
+import org.junit.jupiter.api.Test;
+
+public class TestMatchTDB2 {
+    @Test
+    public void testRdfsOnNodeIdLevel() {
+        Graph schema = SSE.parseGraph("(graph (rdf:type rdf:type rdf:Property) 
(:p rdfs:domain :C) )");
+
+        Dataset baseDs = TDB2Factory.createDataset();
+        DatasetGraph baseDsg = baseDs.asDatasetGraph();
+
+        MapperX<NodeId, Tuple3<NodeId>> mapper = MapperXTDB.create(baseDsg);
+
+        try (AutoTxn txn = Txn.autoTxn(baseDsg, ReadWrite.WRITE)) {
+            // !!! The schema must be added first, otherwise we won't have 
NodeIds !!!
+            // !!! Also, all terms (especially rdf:type) must have 
corresponding NodeIds !!!
+            G.addInto(baseDsg.getDefaultGraph(), schema);
+            ConfigRDFS<NodeId> configRDFS = RDFSFactory.setupRDFS(schema, 
mapper);
+
+            // Add wrapping on NodeId level.
+            DatasetGraph rdfsDsg = new DatasetGraphWithGraphTransform(baseDsg,
+                g -> GraphMatch.adapt(g, new MatchRDFSWrapper<>(configRDFS, 
MatchTDB.wrap(g))));
+
+            // Add data.
+            Graph data = SSE.parseGraph("(graph (:s :p :o) )");
+            G.addInto(rdfsDsg.getDefaultGraph(), data);
+
+            // Execute queries and compare.
+            Graph expectedGraph = SSE.parseGraph("(graph (:s rdf:type :C) )");
+            Graph actualGraph = QueryExec.dataset(rdfsDsg).query("CONSTRUCT 
WHERE { <http://example/s> a ?o }").construct();
+
+            Set<Triple> expected = Iter.toSet(expectedGraph.find());
+            Set<Triple> actual = Iter.toSet(actualGraph.find());
+
+            assertEquals(expected, actual);
+            txn.commit();
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index 6ad43872b7..9d4af1b85e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -89,6 +89,7 @@
     <ver.commons-io>2.21.0</ver.commons-io>
     <ver.commons-cli>1.7.0</ver.commons-cli>
     <ver.commons-lang3>3.20.0</ver.commons-lang3>
+    <ver.commons-math4>4.0-beta1</ver.commons-math4>
     <ver.commons-rdf>0.5.0</ver.commons-rdf>
     <ver.commons-csv>1.14.1</ver.commons-csv>
     <ver.commons-codec>1.20.0</ver.commons-codec>
@@ -422,6 +423,12 @@
         <version>${ver.commons-lang3}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-math4-legacy</artifactId>
+        <version>${ver.commons-math4}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-collections4</artifactId>
@@ -949,7 +956,7 @@
             <release>${java.version}</release>
             <compilerArgs>
               <arg>-proc:none</arg>
-              <!-- 
+              <!--
                    In preparation for Jena6, silence deprecation=removal 
warnings.
                    There are many.
                    Remove Xlint setting at or after Jena6.

Reply via email to